From 771ee626338c8fa00f8eedda9bdfbbddb5206491 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 21 Mar 2009 19:29:44 -0700 Subject: Handle Rack multivalue headers correctly Rack uses a single newline character to represent multi-value headers. Thus { 'Set-Cookie' => "foo=bar\nbar=foo" } will get you: Set-Cookie: foo=bar Set-Cookie: bar=foo While RFC2616 says you can combine headers as: Set-Cookie: foo=bar,bar=foo There are probably HTTP clients out there that don't handle things correctly so don't bother... Additionally, don't bother doing duplicate suppression anymore. Just assume Rack or a higher layer knows what it's doing regarding duplicates and we'll get a Hash most of the time anyways. --- lib/unicorn/http_response.rb | 27 +++++++++++---------------- test/unit/test_response.rb | 7 +++++++ 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/lib/unicorn/http_response.rb b/lib/unicorn/http_response.rb index 2635f14..c8aa3f9 100644 --- a/lib/unicorn/http_response.rb +++ b/lib/unicorn/http_response.rb @@ -21,30 +21,25 @@ module Unicorn class HttpResponse - # headers we allow duplicates for - ALLOWED_DUPLICATES = { - 'Set-Cookie' => true, - 'Set-Cookie2' => true, - 'Warning' => true, - 'WWW-Authenticate' => true, - }.freeze + # Rack does not set/require a Date: header. We always override the + # Connection: and Date: headers no matter what (if anything) our + # Rack application sent us. + SKIP = { 'connection' => true, 'date' => true }.freeze # writes the rack_response to socket as an HTTP response def self.write(socket, rack_response) status, headers, body = rack_response + out = [ "Date: #{Time.now.httpdate}" ] - # Rack does not set/require Date, but don't worry about Content-Length - # since Rack applications that conform to Rack::Lint enforce that - out = [ "#{Const::DATE}: #{Time.now.httpdate}" ] - sent = { Const::CONNECTION => true, Const::DATE => true } - + # Don't bother enforcing duplicate supression, it's a Hash most of + # the time anyways so just hope our app knows what it's doing headers.each do |key, value| - if ! sent[key] || ALLOWED_DUPLICATES[key] - sent[key] = true - out << "#{key}: #{value}" - end + next if SKIP.include?(key.downcase) + value.split(/\n/).each { |v| out << "#{key}: #{v}" } end + # Rack should enforce Content-Length or chunked transfer encoding, + # so don't worry or care about them. socket_write(socket, "HTTP/1.1 #{status} #{HTTP_STATUS_CODES[status]}\r\n" \ "Connection: close\r\n" \ diff --git a/test/unit/test_response.rb b/test/unit/test_response.rb index c30a141..4c7423b 100644 --- a/test/unit/test_response.rb +++ b/test/unit/test_response.rb @@ -41,5 +41,12 @@ class ResponseTest < Test::Unit::TestCase io.rewind assert_match(/.* #{HTTP_STATUS_CODES[code]}$/, io.readline.chomp, "wrong default reason phrase") end + + def test_rack_multivalue_headers + out = StringIO.new + HttpResponse.write(out,[200, {"X-Whatever" => "stuff\nbleh"}, []]) + assert_match(/^X-Whatever: stuff\r\nX-Whatever: bleh\r\n/, out.string) + end + end -- cgit v1.2.3-24-ge0c7