Red Green Repeat Adventures of a Spec Driven Junkie

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.

Prayer Book - Arganonä Maryam - Attributed to Baselyos (The Ground Hornbill Master)

Requirements

If you would like to follow along:

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

Viewing Headers in Vivaldi

$ 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.