Rails Custom Response Headers
I want to set custom headers in the response within my Rails application. This is a “secret channel” to communicate additional information within the web request.
Requirements
If you would like to follow along:
- Install Virtualbox
- Install vagrant
- Clone this folder
Once cloned, run: $ vagrant up
to create the Virtualbox that canrun
this project.
After the Virtualbox finishes building, run: $ vagrant ssh
to log
into the virtualbox computer and to get to the project resources, run
command: cd /vagrant
.
Run the sample_app
The vagrant script sets up a sample_app
in the /vagrant/sample_app
folder. This is a clean Rails app and we will use to check how
things work in a pristine environment.
vagrant@ubuntu-xenial:/vagrant/sample_app$ rails s
=> Booting Puma
=> Rails 5.2.2 application starting in development
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.12.0 (ruby 2.5.3-p105), codename: Llamas in Pajamas
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop
Seeing headers
I have never had to examine HTTP headers before, but these headers are everywhere! With every request, the server adds headers and are transparent to the web experience.
There are two way to see headers: through the web browser and curl. I will show how both works to verify the same headers are coming through both.
Inspector
In a web browser like Chrome, or in my case,
Vivaldi, on the page, open up the Inspector
from the right menu command or opening the menu option: Tools
->
Developer Tools
$ curl -I
Another option I like is to use the $ curl
command. To have the curl
command only return headers, use the -I
option (note: capital i,
not a lowercase l):
vagrant@ubuntu-xenial:/$ curl -I localhost:3000
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Referrer-Policy: strict-origin-when-cross-origin
Content-Type: text/html; charset=utf-8
ETag: W/"d904c06f68a86891d7f7c4a85f2ad100"
Cache-Control: max-age=0, private, must-revalidate
Content-Security-Policy: script-src 'unsafe-inline'; style-src 'unsafe-inline'
X-Request-Id: bc9d9cbc-96ed-4657-8803-b9c296eb7c10
X-Runtime: 0.085410
The details are equivalent between the $ curl
command and the
browser details, except presentation format.
For the remainder of the article, I will use the curl command instead
of the web browser. It’s just easier to display $ curl
output
instead of browser output.
Add Controller
Let’s make a new controller to receive a request:
vagrant@ubuntu-xenial:/vagrant/sample_app$ rails generate controller test
Running via Spring preloader in process 2874
create app/controllers/test_controller.rb
invoke erb
create app/views/test
invoke test_unit
create test/controllers/test_controller_test.rb
invoke helper
create app/helpers/test_helper.rb
invoke test_unit
invoke assets
invoke coffee
create app/assets/javascripts/test.coffee
invoke scss
create app/assets/stylesheets/test.scss
Modify routes.rb
To make the new /test
endpoint accessible, we must make an entry to
the routes.rb
file.
Rails.application.routes.draw do
get '/test', to: 'test#index'
end
Add index to controller
The controller needs the corresponding method specified in the
routes.rb
file, which is an index
method:
class TestController < ApplicationController
def index
render json: 'test'
end
end
Test again
Let’s make sure the new endpoint is working by checking its response:
vagrant@ubuntu-xenial:~$ curl localhost:3000/test
testvagrant@ubuntu-xenial:~$
Now let’s check its headers with $ curl -I
:
vagrant@ubuntu-xenial:~$ curl -I localhost:3000/test
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Referrer-Policy: strict-origin-when-cross-origin
Content-Type: application/json; charset=utf-8
ETag: W/"9f86d081884c7d659a2feaa0c55ad015"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 00dff4b4-e96f-485c-b537-d9e0f5a722b5
X-Runtime: 0.015437
Cool, pretty straight forward.
Custom Header in Test
To add just a header into the /test
controller, add the desired
header the response.headers
field in the following format:
response.headers['desired_header'] = 'desired_header_value'
As a convenience, using an after_action
for the controller will add
the desired header to every endpoint supported by that controller:
1
2
3
4
5
6
7
8
9
10
11
12
class TestController < ApplicationController
after_action :add_headers
def index
render json: 'test'
end
def add_headers
response.headers['test-controller-header'] = 'test-controller-header-value'
end
end
To see the new headers, use $ curl -I
command:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
vagrant@ubuntu-xenial:~$ curl -I localhost:3000/test
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Referrer-Policy: strict-origin-when-cross-origin
Content-Type: application/json; charset=utf-8
test-controller-header: test-controller-header-value
ETag: W/"9f86d081884c7d659a2feaa0c55ad015"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: e456d916-2b19-4b00-a107-83087bde7e1a
X-Runtime: 0.008259
The new header: test-controller-header
shows up on line 10.
The relevant line:
test-controller-header: test-controller-header-value
Magic!
Custom Header Every Endpoint
To have custom headers on every controller, add a similar function
to the ApplicationController
file:
app/controller/application_controller.rb
file:
class ApplicationController < ActionController::Base
after_action :custom_headers
def custom_headers
response.headers['X-Every-Endpoint-Custom-Headers'] = 'every endpoint custom header value'
end
end
Test again using: $ curl -I
command:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
vagrant@ubuntu-xenial:~$ curl -I localhost:3000/test
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Referrer-Policy: strict-origin-when-cross-origin
Content-Type: application/json; charset=utf-8
test-controller-header: test-controller-header-value
X-Every-Endpoint-Custom-Headers: every endpoint custom header value
ETag: W/"9f86d081884c7d659a2feaa0c55ad015"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: ea1752cd-6831-4eb5-9382-8efde4c423f6
X-Runtime: 0.105045
On line 11, the X-Every-Endpoint-Custom-Headers
appears with the
test-controller-header
on line 10. The former is from the
application_controller
and the latter is from the test_controller
.
The relevant parts:
test-controller-header: test-controller-header-value
X-Every-Endpoint-Custom-Headers: every endpoint custom header value
Magical!
Conclusion
Setting up Rails to have a “secret channel” for communicating
information in a response is easy. Modify the response.headers
object from the controller.
Adding information to every controller is as easy as modifying the
response.headers
object from the application_controller.
I will take advantage of this feature whenever I need to have a “secret channel” in the server response.