5

I have a requirement to send an HTTP header in a specific character-case. I am aware that this is against the RFC, but I have a requirement.

http.get seems to change the case of the headers dictionary I supply it. How can I preserve the character-case?

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
Yaron Naveh
  • 23,560
  • 32
  • 103
  • 158
  • Often requirements are (at least partly) debatable. And if your "requirement" is to violate the HTTP spec, then you should debate the hell out of your client. Things **will** along the way and you will have a hard time adapting to every new component you introduce. That includes things like proxies, loadbalancers, firewalls, and webservers. All those would have to work with your changes which is really hard to achieve in every case. You should try to find another solution or you will suffer from pain for ever after :) – Holger Just Jan 15 '12 at 09:02
  • I need to interoperate with a 3rd party system that violates the rfc. Not much I can do. – Yaron Naveh Jan 15 '12 at 10:14
  • 1
    [FIX ALL THE SYSTEMS](http://i.stack.imgur.com/UXBEb.jpg) – Holger Just Jan 15 '12 at 10:28

5 Answers5

15

Based on the Tin Man's answer that the Net::HTTP library is calling #downcase on your custom header key (and all header keys), here are some additional options that don't monkey-patch the whole of Net::HTTP.

You could try this:

custom_header_key = "X-miXEd-cASe"
def custom_header_key.downcase
  self
end

To avoid clearing the method cache, either store the result of the above in a class-level constant:

custom_header_key = "X-miXEd-cASe"
def custom_header_key.downcase
  self
end
CUSTOM_HEADER_KEY = custom_header_key

or subclass String to override that particular behavior:

class StringWithIdentityDowncase < String
  def downcase
    self
  end
end

custom_header_key = StringWithIdentityDowncase.new("X-miXEd-cASe")
yfeldblum
  • 65,165
  • 12
  • 129
  • 169
  • +1 I didn't think about turning off the `downcase` method, but that's what this basically does. This gets my vote as the correct answer. The monkey patching has too much code smell. – the Tin Man Jan 15 '12 at 10:01
  • 1
    I tip my hat to you sir! One note: I had to override the "capitalize" method in the same way as the downcase method for it to work. – codingFoo Dec 04 '12 at 15:22
  • As of Ruby 2.6 I had to override `capitalize` and `to_s` also – Camilo Sanchez Feb 26 '20 at 14:41
  • This is disgusting, but as of Ruby 3.0 I had to override `split` too... ```def split(m) super.map { |s| self.class.new(s) } end ``` – Felipe Zavan Mar 15 '21 at 17:37
6

The accepted answer does not work. Frankly, I doubt that it ever did since it looks like it would have had to also override split and capitalize, I followed that method back a few commits, it's been that way at least since 2004.

Here is my solution, in answer to this closed question:

require 'net/http'

class Net::HTTP::ImmutableHeaderKey
  attr_reader :key

  def initialize(key)
    @key = key
  end

  def downcase
    self
  end

  def capitalize
    self
  end

  def split(*)
    [self]
  end

  def hash
    key.hash
  end

  def eql?(other)
    key.eql? other.key.eql?
  end

  def to_s
    key
  end
end

Now you need to be sure to always use instances of this class as your keys.

request           = Net::HTTP::Get.new('/')
user_key          = Net::HTTP::ImmutableHeaderKey.new("user")
request[user_key] = "James"

require 'stringio'
StringIO.new.tap do |output|
  request.exec output, 'ver', 'path'
  puts output.string
end

# >> GET path HTTP/ver
# >> Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3
# >> Accept: */*
# >> User-Agent: Ruby
# >> user: James
# >> 
Community
  • 1
  • 1
Joshua Cheek
  • 30,436
  • 16
  • 74
  • 83
2

Mine is one way to do it, but I recommend doing it as @yfeldblum recommends, simply short-circuit downcase for the header keys that need to have their case left-alone.


In multiple places in Net::HTTP::HTTPHeader the headers get folded to lower-case using downcase.

I think it is pretty drastic to change that behavior, but this will do it. Add this to your source and it will redefine the methods in the HTTPHeader module that had downcase in them.

module HTTPHeader

  def initialize_http_header(initheader)
    @header = {}
    return unless initheader
    initheader.each do |key, value|
      warn "net/http: warning: duplicated HTTP header: #{key}" if key?(key) and $VERBOSE
      @header[key] = [value.strip]
    end
  end

  def [](key)
    a = @header[key] or return nil
    a.join(', ')
  end

  def []=(key, val)
    unless val
      @header.delete key
      return val
    end
    @header[key] = [val]
  end

  def add_field(key, val)
    if @header.key?(key)
      @header[key].push val
    else
      @header[key] = [val]
    end
  end

  def get_fields(key)
    return nil unless @header[key]
    @header[key].dup
  end

  def fetch(key, *args, &block)   #:yield: +key+
    a = @header.fetch(key, *args, &block)
    a.kind_of?(Array) ? a.join(', ') : a
  end

  # Removes a header field.
  def delete(key)
    @header.delete(key)
  end

  # true if +key+ header exists.
  def key?(key)
    @header.key?(key)
  end

  def tokens(vals)
    return [] unless vals
    vals.map {|v| v.split(',') }.flatten\
    .reject {|str| str.strip.empty? }\
    .map {|tok| tok.strip }
  end

end

I think this is a brute force way of going about it, but nothing else more elegant jumped to mind.

While this should fix the problem for any Ruby libraries using Net::HTTP, it will probably fail for any gems that use Curl or libcurl.

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
1

Joshua Cheek's answer is great, but it does in work anymore in Ruby 2.3

This modification fix it:

class Net::HTTP::ImmutableHeaderKey
  ...

  def to_s
    caller.first.match(/capitalize/) ? self : @key
  end
end
0

It all falls down into the net/generic_request#write_header. You could monkey patch the code

# 'net/generic_request' line 319
def write_header(sock, ver, path)
  customheaders = {
    "My-Custom-Header" => "MY-CUSTOM-HEADER",
    "Another-Custom-Header" => "aNoThErCuStOmHeAdEr"
  }
  buf = "#{@method} #{path} HTTP/#{ver}\r\n"
  each_capitalized do |k,v|
    customheaders.key?(k) ? kk = customheaders[k] : kk = k
    buf << "#{kk}: #{v}\r\n"
  end
  buf << "\r\n"
  sock.write buf
end

and you don't need to rewrite the whole net/http/header, net/generic_request and net/http chain. It's not the best solution, but it's the easiest one I guess and there's least amount of monkey patching.

Hope it helps.

user1164108
  • 101
  • 1