36

The title is pretty self-explanatory. Is there any way to get the headers (except for Rack::Request.env[])?

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
PJK
  • 2,082
  • 3
  • 17
  • 28

3 Answers3

42

The HTTP headers are available in the Rack environment passed to your app:

HTTP_ Variables: Variables corresponding to the client-supplied HTTP request headers (i.e., variables whose names begin with HTTP_). The presence or absence of these variables should correspond with the presence or absence of the appropriate HTTP header in the request.

So the HTTP headers are prefixed with "HTTP_" and added to the hash.

Here's a little program that extracts and displays them:

require 'rack'

app = Proc.new do |env|
  headers = env.select {|k,v| k.start_with? 'HTTP_'}
    .collect {|key, val| [key.sub(/^HTTP_/, ''), val]}
    .collect {|key, val| "#{key}: #{val}<br>"}
    .sort
  [200, {'Content-Type' => 'text/html'}, headers]
end

Rack::Server.start :app => app, :Port => 8080

When I run this, in addition to the HTTP headers as shown by Chrome or Firefox, there is a "VERSION: HTPP/1.1" (i.e. an entry with key "HTTP_VERSION" and value "HTTP/1.1" is being added to the env hash).

matt
  • 78,533
  • 8
  • 163
  • 197
  • 3
    Ah, so it's basically `env` anyway :). What I dislike are the upcased names with some chars replaced. Well, I guess I will have to get away with it.... – PJK Jun 11 '11 at 21:34
  • @PJK well the names should be case insensitive anyway: http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2. What characters are being replaced? Are you trying to use characters from outside the ASCII chracter set? Header names should be ASCII only. – matt Jun 11 '11 at 22:16
  • 2
    I know, it is just a matter of convenience... For instance, X-Build becomes HTTP_X_BUILD, which means X_Build and X-BUILD should be equivalent but (I've been told that) browsers differentiate between these two alternatives. – PJK Jun 11 '11 at 22:29
  • 3
    @PJK I see: `-` is being changed to `_`. I guess that's to remain compatible with CGI (an environment variable can't contain `-`). But the _response_ headers shouldn't be affected. – matt Jun 11 '11 at 23:47
  • Link to Rack environment documentation that's not broken: https://github.com/rack/rack/blob/master/SPEC.rdoc#the-environment- – salomvary Jan 14 '21 at 11:36
7

Based on @matt's answer, but this really gives you the request headers in a hash as requested in the question:

headers = Hash[*env.select {|k,v| k.start_with? 'HTTP_'}
  .collect {|k,v| [k.sub(/^HTTP_/, ''), v]}
  .collect {|k,v| [k.split('_').collect(&:capitalize).join('-'), v]}
  .sort
  .flatten]

Depending on what key convention you prefer you might want to use something else instead of :capitalize.

Gavriel
  • 18,880
  • 12
  • 68
  • 105
6

Like @Gavriel's answer, but using transform_keys (simpler):

class Request
  def headers
    env.select { |k,v| k.start_with? 'HTTP_'}.
      transform_keys { |k| k.sub(/^HTTP_/, '').split('_').map(&:capitalize).join('-') }
  end
end

You can even make it so lookups still work even if the case is different:

  def headers
    env.
      select { |k,v| k.start_with? 'HTTP_'}.
      transform_keys { |k| k.sub(/^HTTP_/, '').split('_').map(&:capitalize).join('-') }.
      sort.to_h.
      tap do |headers|
        headers.define_singleton_method :[] do |k|
          super(k.split(/[-_]/).map(&:capitalize).join('-'))
        end
      end
  end

So for example, even if headers normalizes the keys so it returns this:

{
  Dnt: '1',
  Etag: 'W/"ec4454af5ae1bacff1afc5a06a2133f4"',
  'X-Xss-Protection': '1; mode=block',
}

you can still look up headers using the more natural/common names for these headers:

headers['DNT']
headers['ETag']
headers['X-XSS-Protection']
Tyler Rick
  • 9,191
  • 6
  • 60
  • 60
  • in your example, shouldn't it be `'Dnt': '1'` instead of `Dnt: '1'`? – Ron Klein May 21 '19 at 06:13
  • @RonKlein `Dnt:` is correct. Using `'Dnt':` is JSON not ruby. – Marlin Pierce Aug 13 '19 at 20:01
  • @MarlinPierce Well it is syntactically correct Ruby but it's not what the code produces. @RonKlein is right, it should be `'Dnt': '1',` The keys here are strings from start to end. Capitalized literals denote constants in Ruby. – Arnaud Meuret Nov 05 '19 at 10:42
  • @ArnaudMeuret Now, I think you are mixing up Constants and Literals. In ruby, `'Dnt':` resolves to the symbol :Dnt. If you want string keys, you need `{ 'Dnt' => '1' }`. This will show you that `'Dnt':` is a symbol, `{ 'Dnt': '1' }.each_pair { |key, value| puts key.inspect }`. – Marlin Pierce Nov 06 '19 at 18:49
  • @ArnaudMeuret another way to see this, is that `Dnt='Knock';{ 'Dnt': 1, 'Dnt' => 2, Dnt => 3 }` evaluates to `{:Dnt=>1, "Dnt"=>2, "Knock"=>3}`. – Marlin Pierce Nov 06 '19 at 18:56
  • @MarlinPierce Mmm I'm not. But, with the help of the faulty syntax highlighter, I did overlook that Tyler used the : mapping and misstook it for =>. My point remains that I don't see where his function produces these symbol keys, which you can not look up using strings. – Arnaud Meuret Nov 08 '19 at 03:34
  • now that Rack is adopting all-downcase headers, I believe it would be a good idea to remove the "capitalize" from your code – Everton J. Carpes Sep 23 '22 at 14:59