Daniel Doubrovkine bio photo

Daniel Doubrovkine

aka dB., @awscloud, former CTO @artsy, +@vestris, NYC

Email Twitter LinkedIn Github Strava

I recently helped debug Grape#1880, an issue a developer had with HTTP headers in Grape and Rack. It wasn’t immediately obvious.

Test API

Let’s write a simple Grape API that returns a value for a header.

module Acme
  class Headers < Grape::API
    format :json

    desc 'Returns a header value.'
    params do
      requires :key, type: String
    end
    get 'headers/:key' do
      key = params[:key]
      { key => headers[key] }
    end
  end
end

Default Headers

The default headers in a Rack test are Cookie and Host.

  it 'returns all headers' do
    get '/api/headers'
    expect(JSON.parse(last_response.body)).to eq(
      'Cookie' => '',
      'Host' => 'example.org'
    )
  end

  it 'returns a Host header' do
    get '/api/headers/Host'
    expect(JSON.parse(last_response.body)).to eq('Host' => 'example.org')
  end

Curl sends more headers by default.

$ curl http://localhost:9292/api/headers

{"Host":"localhost:9292","User-Agent":"curl/7.54.0","Accept":"*/*","Version":"HTTP/1.1"}

Pascal Case Conversion

Headers in Grape are always converted to pascal-case.

$ curl -H eLiTe:42 http://localhost:9292/api/headers/Elite

{"Elite":"42"}

This means that a pascal-case-looking header ReticulatedSpline is converted to Reticulatedspline.

curl -H ReticulatedSpline:42 http://localhost:9292/api/headers/Reticulatedspline
{"Reticulatedspline":"42"}

And a lowercase reticulated-spline is converted to Reticulated-Spline, similarly to User-Agent.

curl -H reticulated-spline:42 http://localhost:9292/api/headers/Reticulated-Spline 
{"Reticulated-Spline":"42"}

Rack

Rack stores HTTP headers in ENV as all uppercase with an HTTP_ prefix. You can pass the Rack env as the the second parameter in your specs. In the example below HTTP_RETICULATED_SPLINE becomes Reticulated-Spline and SOMETHING_ELSE is only available in ENV['SOMETHING_ELSE'] and is not a header.

get '/api/headers', nil, { 
  'HTTP_RETICULATED_SPLINE' => 42, 
  'SOMETHING_ELSE' => 1 
}
expect(JSON.parse(last_response.body)).to eq(
  'Cookie' => '', 
  'Host' => 'example.org', 
  'Reticulated-Spline' => 42
)
end

To avoid confusion use the header helper that behaves as one would expect.

header 'Reticulated-Spline', 42
get '/api/headers/Reticulated-Spline'
expect(JSON.parse(last_response.body)).to eq('Reticulated-Spline' => 42)

Rails

Rails get takes a Hash, but it’s still a wrapper on top of Rack, sort of. You can specify a header as Header or HTTP_....

get '/api/headers', headers: {
  'HTTP_RETICULATED_SPLINE' => 42,
  'Something' => 1,
  'SOMETHING_ELSE' => 1
}
expect(JSON.parse(response.body)).to eq(
  'Accept' => 'text/xml,image/png,*/*;q=0.5',
  'Cookie' => '',
  'Host' => 'www.example.com',
  'Reticulated-Spline' => 42,
  'Something' => 1
)