Permalink
Browse files

Add Support for Chunked transfer encoding transport.

  Example:

  class ChunkedAction < Cramp::Action
    self.transport = :chunked
    on_start :send_data

    def send_data
      render "Chunk 1"
      render "Chunk 2"
    end
  end
  • Loading branch information...
1 parent 4d63176 commit da4f7700502645400d6751685baaf6c91b6bb341 @lifo committed Aug 12, 2011
Showing with 126 additions and 11 deletions.
  1. +14 −0 CHANGELOG
  2. +28 −0 examples/chunked.ru
  3. +1 −0 lib/cramp.rb
  4. +11 −4 lib/cramp/abstract.rb
  5. +36 −7 lib/cramp/action.rb
  6. +36 −0 test/controller/chunked_transport_test.rb
View
@@ -1,5 +1,19 @@
== 0.15 Crazy Apes (Unreleased)
+* Support for Chunked transfer encoding transport.
+
+ Example:
+
+ class ChunkedAction < Cramp::Action
+ self.transport = :chunked
+ on_start :send_data
+
+ def send_data
+ render "Chunk 1"
+ render "Chunk 2"
+ end
+ end
+
== 0.14 Asgard (5 August, 2011)
* Add an option to the application generator for configuring async Active Record.
View
@@ -0,0 +1,28 @@
+require "rubygems"
+require "bundler"
+Bundler.setup(:default, :example)
+
+require 'cramp'
+require 'thin'
+
+class Chunked < Cramp::Action
+ self.transport = :chunked
+
+ on_start :send_data
+ periodic_timer :close_stream, :every => 3
+
+ def send_data
+ 3.times { render Time.now.to_s }
+ end
+
+ def close_stream
+ render "That's all folks!"
+ finish
+ end
+end
+
+# You can test this from the terminal using curl
+# $ curl -N http://0.0.0.0:3000/
+
+# bundle exec thin -V -R examples/chunked.ru start
+run Chunked
View
@@ -10,6 +10,7 @@
require 'active_support/core_ext/kernel/reporting'
require 'active_support/concern'
require 'active_support/core_ext/hash/indifferent_access'
+require 'active_support/core_ext/hash/except'
require 'active_support/buffered_logger'
require 'rack'
View
@@ -37,14 +37,14 @@ def continue
end
def send_headers
- status, headers = respond_with
+ status, headers = build_headers
send_initial_response(status, headers, @body)
rescue StandardError, LoadError, SyntaxError => exception
handle_exception(exception)
end
- def respond_with
- [200, {'Content-Type' => 'text/html'}]
+ def build_headers
+ respond_to?(:respond_with, true) ? respond_with : [200, {'Content-Type' => 'text/html'}]
end
def init_async_body
@@ -61,7 +61,7 @@ def finished?
end
def finish
- @body.succeed if !finished? && @body && !@body.closed?
+ @body.succeed if is_finishable?
ensure
@_state = :finished
@finished = true
@@ -90,5 +90,12 @@ def params
def route_params
@env['router.params'] || @env['usher.params']
end
+
+ private
+
+ def is_finishable?
+ !finished? && @body && !@body.closed?
+ end
+
end
end
View
@@ -14,19 +14,31 @@ def render(body, *args)
send(:"render_#{transport}", body, *args)
end
- def send_initial_response(*)
+ def send_initial_response(status, headers, body)
case transport
when :long_polling
- # Dont send no initial response
+ # Dont send no initial response. Just cache it for later.
+ @_lp_status = status
+ @_lp_headers = headers
else
super
end
end
- def respond_with
+ class_attribute :default_sse_headers
+ self.default_sse_headers = {'Content-Type' => 'text/event-stream', 'Cache-Control' => 'no-cache', 'Connection' => 'keep-alive'}
+
+ class_attribute :default_chunked_headers
+ self.default_chunked_headers = {'Transfer-Encoding' => 'chunked', 'Connection' => 'keep-alive'}
+
+ def build_headers
case transport
when :sse
- [200, {'Content-Type' => 'text/event-stream', 'Cache-Control' => 'no-cache', 'Connection' => 'keep-alive'}]
+ status, headers = respond_to?(:respond_with, true) ? respond_with : [200, {'Content-Type' => 'text/html'}]
+ [status, headers.merge(self.default_sse_headers)]
+ when :chunked
+ status, headers = respond_to?(:respond_with, true) ? respond_with : [200, {}]
+ [status, headers.merge(self.default_chunked_headers)]
else
super
end
@@ -37,10 +49,9 @@ def render_regular(body, *)
end
def render_long_polling(data, *)
- status, headers = respond_with
- headers['Content-Length'] = data.size.to_s
+ @_lp_headers['Content-Length'] = data.size.to_s
- send_response(status, headers, @body)
+ send_response(@_lp_status, @_lp_headers, @body)
@body.call(data)
finish
@@ -66,6 +77,15 @@ def render_websocket(body, *)
@body.call(data)
end
+ CHUNKED_TERM = "\r\n"
+ CHUNKED_TAIL = "0#{CHUNKED_TERM}#{CHUNKED_TERM}"
+
+ def render_chunked(body, *)
+ data = [Rack::Utils.bytesize(body).to_s(16), CHUNKED_TERM, body, CHUNKED_TERM].join
+
+ @body.call(data)
+ end
+
# Used by SSE
def sse_event_id
@sse_event_id ||= Time.now.to_i
@@ -77,6 +97,15 @@ def encode(string, encoding = 'UTF-8')
protected
+ def finish
+ case transport
+ when :chunked
+ @body.call(CHUNKED_TAIL) if is_finishable?
+ end
+
+ super
+ end
+
def websockets_protocol_10?
[8, 9, 10].include?(@env['HTTP_SEC_WEBSOCKET_VERSION'].to_i)
end
@@ -0,0 +1,36 @@
+require 'test_helper'
+
+class ChunkedTransportTest < Cramp::TestCase
+
+ class ChunkedAction < Cramp::Action
+ self.transport = :chunked
+ on_start :send_chunks
+
+ def send_chunks
+ render "Hello"
+ render "World!"
+ finish
+ end
+ end
+
+ def app
+ ChunkedAction
+ end
+
+ def test_headers
+ get '/' do |status, headers, body|
+ assert_equal 200, status
+ assert_equal "chunked", headers["Transfer-Encoding"]
+ assert_kind_of Cramp::Body, body
+
+ EM.stop
+ end
+ end
+
+ def test_body
+ get_body_chunks '/', :count => 3 do |chunks|
+ assert_equal ["5\r\nHello\r\n", "6\r\nWorld!\r\n", "0\r\n\r\n"], chunks
+ end
+ end
+
+end

0 comments on commit da4f770

Please sign in to comment.