From a1c7992d47ac820c64604b8aeb8779a0bf741fcf Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 5 Feb 2009 15:16:18 -0800 Subject: Simplify HttpResponse since we only handle Rack now The previous API was very flexible, but I don't think many people really cared for it... We now repeatedly use the same HeaderOut in each process since I completely don't care for multithreading. --- lib/unicorn.rb | 2 +- lib/unicorn/const.rb | 1 + lib/unicorn/header_out.rb | 3 +- lib/unicorn/http_response.rb | 156 ++++++++----------------------------------- test/unit/test_response.rb | 10 +-- 5 files changed, 33 insertions(+), 139 deletions(-) diff --git a/lib/unicorn.rb b/lib/unicorn.rb index 57471a4..295327b 100644 --- a/lib/unicorn.rb +++ b/lib/unicorn.rb @@ -125,7 +125,7 @@ module Unicorn # in the case of large file uploads the user could close the socket, so skip those requests break if request.body == nil # nil signals from HttpRequest::initialize that the request was aborted app_response = @app.call(request.env) - response = HttpResponse.new(client, app_response).start + HttpResponse.send(client, app_response) break #done else # Parser is not done, queue up more data to read and continue parsing diff --git a/lib/unicorn/const.rb b/lib/unicorn/const.rb index b44bb29..8e52d4b 100644 --- a/lib/unicorn/const.rb +++ b/lib/unicorn/const.rb @@ -113,6 +113,7 @@ module Unicorn HTTP_IF_NONE_MATCH="HTTP_IF_NONE_MATCH".freeze REDIRECT = "HTTP/1.1 302 Found\r\nLocation: %s\r\nConnection: close\r\n\r\n".freeze HOST = "HOST".freeze + CONNECTION = "Connection".freeze end end diff --git a/lib/unicorn/header_out.rb b/lib/unicorn/header_out.rb index b0f66b9..95f2633 100644 --- a/lib/unicorn/header_out.rb +++ b/lib/unicorn/header_out.rb @@ -15,13 +15,14 @@ module Unicorn }.freeze def initialize - @sent = {} + @sent = { Const::CONNECTION => true } @out = [] end def reset! @sent.clear @out.clear + @sent[Const::CONNECTION] = true end def merge!(hash) diff --git a/lib/unicorn/http_response.rb b/lib/unicorn/http_response.rb index f4b30fd..4ffe64b 100644 --- a/lib/unicorn/http_response.rb +++ b/lib/unicorn/http_response.rb @@ -1,143 +1,39 @@ module Unicorn - # Writes and controls your response to the client using the HTTP/1.1 specification. + + # Writes a Rack response to your client using the HTTP/1.1 specification. # You use it by simply doing: # - # response.start(200) do |head,out| - # head['Content-Type'] = 'text/plain' - # out.write("hello\n") - # end - # - # The parameter to start is the response code--which Unicorn will translate for you - # based on HTTP_STATUS_CODES. The head parameter is how you write custom headers. - # The out parameter is where you write your body. The default status code for - # HttpResponse.start is 200 so the above example is redundant. - # - # As you can see, it's just like using a Hash and as you do this it writes the proper - # header to the output on the fly. You can even intermix specifying headers and - # writing content. The HttpResponse class with write the things in the proper order - # once the HttpResponse.block is ended. + # status, headers, body = rack_app.call(env) + # HttpResponse.send(socket, [ status, headers, body ]) # - # You may also work the HttpResponse object directly using the various attributes available - # for the raw socket, body, header, and status codes. If you do this you're on your own. - # A design decision was made to force the client to not pipeline requests. HTTP/1.1 - # pipelining really kills the performance due to how it has to be handled and how - # unclear the standard is. To fix this the HttpResponse gives a "Connection: close" - # header which forces the client to close right away. The bonus for this is that it - # gives a pretty nice speed boost to most clients since they can close their connection - # immediately. + # Most header correctness (including Content-Length) is the job of + # Rack, with the exception of the "Connection: close" and "Date" + # headers. # - # One additional caveat is that you don't have to specify the Content-length header - # as the HttpResponse will write this for you based on the out length. - class HttpResponse - attr_reader :socket - attr_reader :body - attr_writer :body - attr_reader :header - attr_reader :status - attr_writer :status - attr_reader :body_sent - attr_reader :header_sent - attr_reader :status_sent - - def initialize(socket, app_response) - @socket = socket - @app_response = app_response - @body = StringIO.new - app_response[2].each {|x| @body << x} - @status = app_response[0] - @reason = nil - @header = HeaderOut.new - @header[Const::DATE] = Time.now.httpdate - @header.merge!(app_response[1]) - @body_sent = false - @header_sent = false - @status_sent = false - end - - # Receives a block passing it the header and body for you to work with. - # When the block is finished it writes everything you've done to - # the socket in the proper order. This lets you intermix header and - # body content as needed. Handlers are able to modify pretty much - # any part of the request in the chain, and can stop further processing - # by simple passing "finalize=true" to the start method. By default - # all handlers run and then mongrel finalizes the request when they're - # all done. - # TODO: docs - def start #(status=200, finalize=false, reason=nil) - finished - end - - # Primarily used in exception handling to reset the response output in order to write - # an alternative response. It will abort with an exception if you have already - # sent the header or the body. This is pretty catastrophic actually. - def reset - if @body_sent - raise "You have already sent the request body." - elsif @header_sent - raise "You have already sent the request headers." - else - # XXX Dubious ( http://mongrel.rubyforge.org/ticket/19 ) - @header.reset! - - @body.close - @body = StringIO.new - end - end + # A design decision was made to force the client to not pipeline or + # keepalive requests. HTTP/1.1 pipelining really kills the + # performance due to how it has to be handled and how unclear the + # standard is. To fix this the HttpResponse always gives a + # "Connection: close" header which forces the client to close right + # away. The bonus for this is that it gives a pretty nice speed boost + # to most clients since they can close their connection immediately. - def send_status(content_length=@body.length) - if not @status_sent - @header['Content-Length'] = content_length if content_length and @status != 304 - write(HTTP_STATUS_HEADERS[@status]) - @status_sent = true - end - end - - def send_header - if not @header_sent - write("#{@header.to_s}#{Const::LINE_END}") - @header_sent = true - end - end - - def send_body - if not @body_sent - @body.rewind - write(@body.read) - @body_sent = true - end - end - - def socket_error(details) - # ignore these since it means the client closed off early - @socket.close rescue nil - done = true - raise details - end + class HttpResponse - def write(data) - @socket.write(data) - rescue => details - socket_error(details) - end + # we'll have one of these per-process + HEADERS = HeaderOut.new unless defined?(HEADERS) - # This takes whatever has been done to header and body and then writes it in the - # proper format to make an HTTP/1.1 response. - def finished - send_status - send_header - send_body - end + def self.send(socket, rack_response) + status, headers, body = rack_response + HEADERS.reset! - # Used during error conditions to mark the response as "done" so there isn't any more processing - # sent to the client. - def done=(val) - @status_sent = true - @header_sent = true - @body_sent = true - end + # Rack does not set Date, but don't worry about Content-Length, + # since Rack enforces that in Rack::Lint + HEADERS[Const::DATE] = Time.now.httpdate + HEADERS.merge!(headers) - def done - (@status_sent and @header_sent and @body_sent) + socket.write("#{HTTP_STATUS_HEADERS[status]}#{HEADERS.to_s}\r\n") + body.each { |chunk| socket.write(chunk) } end end diff --git a/test/unit/test_response.rb b/test/unit/test_response.rb index 1263a49..b142c07 100644 --- a/test/unit/test_response.rb +++ b/test/unit/test_response.rb @@ -12,25 +12,21 @@ class ResponseTest < Test::Unit::TestCase def test_response_headers out = StringIO.new - resp = HttpResponse.new(out,[200, {"X-Whatever" => "stuff"}, ["cool"]]) - resp.finished + HttpResponse.send(out,[200, {"X-Whatever" => "stuff"}, ["cool"]]) assert out.length > 0, "output didn't have data" end def test_response_200 io = StringIO.new - resp = HttpResponse.new(io, [200, {}, []]) - - resp.finished + HttpResponse.send(io, [200, {}, []]) assert io.length > 0, "output didn't have data" end def test_response_with_default_reason code = 400 io = StringIO.new - resp = HttpResponse.new(io, [code, {}, []]) - resp.start + HttpResponse.send(io, [code, {}, []]) io.rewind assert_match(/.* #{HTTP_STATUS_CODES[code]}$/, io.readline.chomp, "wrong default reason phrase") end -- cgit v1.2.3-24-ge0c7