aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_dispatch/http/response.rb
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/action_dispatch/http/response.rb')
-rw-r--r--actionpack/lib/action_dispatch/http/response.rb388
1 files changed, 388 insertions, 0 deletions
diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb
new file mode 100644
index 0000000000..2fab6be1a5
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/response.rb
@@ -0,0 +1,388 @@
+require 'active_support/core_ext/module/attribute_accessors'
+require 'action_dispatch/http/filter_redirect'
+require 'monitor'
+
+module ActionDispatch # :nodoc:
+ # Represents an HTTP response generated by a controller action. Use it to
+ # retrieve the current state of the response, or customize the response. It can
+ # either represent a real HTTP response (i.e. one that is meant to be sent
+ # back to the web browser) or a TestResponse (i.e. one that is generated
+ # from integration tests).
+ #
+ # \Response is mostly a Ruby on \Rails framework implementation detail, and
+ # should never be used directly in controllers. Controllers should use the
+ # methods defined in ActionController::Base instead. For example, if you want
+ # to set the HTTP response's content MIME type, then use
+ # ActionControllerBase#headers instead of Response#headers.
+ #
+ # Nevertheless, integration tests may want to inspect controller responses in
+ # more detail, and that's when \Response can be useful for application
+ # developers. Integration test methods such as
+ # ActionDispatch::Integration::Session#get and
+ # ActionDispatch::Integration::Session#post return objects of type
+ # TestResponse (which are of course also of type \Response).
+ #
+ # For example, the following demo integration test prints the body of the
+ # controller response to the console:
+ #
+ # class DemoControllerTest < ActionDispatch::IntegrationTest
+ # def test_print_root_path_to_console
+ # get('/')
+ # puts response.body
+ # end
+ # end
+ class Response
+ # The request that the response is responding to.
+ attr_accessor :request
+
+ # The HTTP status code.
+ attr_reader :status
+
+ attr_writer :sending_file
+
+ # Get and set headers for this response.
+ attr_accessor :header
+
+ alias_method :headers=, :header=
+ alias_method :headers, :header
+
+ delegate :[], :[]=, :to => :@header
+ delegate :each, :to => :@stream
+
+ # Sets the HTTP response's content MIME type. For example, in the controller
+ # you could write this:
+ #
+ # response.content_type = "text/plain"
+ #
+ # If a character set has been defined for this response (see charset=) then
+ # the character set information will also be included in the content type
+ # information.
+ attr_reader :content_type
+
+ # The charset of the response. HTML wants to know the encoding of the
+ # content you're giving them, so we need to send that along.
+ attr_accessor :charset
+
+ CONTENT_TYPE = "Content-Type".freeze
+ SET_COOKIE = "Set-Cookie".freeze
+ LOCATION = "Location".freeze
+ NO_CONTENT_CODES = [204, 304]
+
+ cattr_accessor(:default_charset) { "utf-8" }
+ cattr_accessor(:default_headers)
+
+ include Rack::Response::Helpers
+ include ActionDispatch::Http::FilterRedirect
+ include ActionDispatch::Http::Cache::Response
+ include MonitorMixin
+
+ class Buffer # :nodoc:
+ def initialize(response, buf)
+ @response = response
+ @buf = buf
+ @closed = false
+ end
+
+ def write(string)
+ raise IOError, "closed stream" if closed?
+
+ @response.commit!
+ @buf.push string
+ end
+
+ def each(&block)
+ @response.sending!
+ x = @buf.each(&block)
+ @response.sent!
+ x
+ end
+
+ def abort
+ end
+
+ def close
+ @response.commit!
+ @closed = true
+ end
+
+ def closed?
+ @closed
+ end
+ end
+
+ # The underlying body, as a streamable object.
+ attr_reader :stream
+
+ def initialize(status = 200, header = {}, body = [])
+ super()
+
+ header = merge_default_headers(header, self.class.default_headers)
+
+ self.body, self.header, self.status = body, header, status
+
+ @sending_file = false
+ @blank = false
+ @cv = new_cond
+ @committed = false
+ @sending = false
+ @sent = false
+ @content_type = nil
+ @charset = nil
+
+ if content_type = self[CONTENT_TYPE]
+ type, charset = content_type.split(/;\s*charset=/)
+ @content_type = Mime::Type.lookup(type)
+ @charset = charset || self.class.default_charset
+ end
+
+ prepare_cache_control!
+
+ yield self if block_given?
+ end
+
+ def await_commit
+ synchronize do
+ @cv.wait_until { @committed }
+ end
+ end
+
+ def await_sent
+ synchronize { @cv.wait_until { @sent } }
+ end
+
+ def commit!
+ synchronize do
+ before_committed
+ @committed = true
+ @cv.broadcast
+ end
+ end
+
+ def sending!
+ synchronize do
+ before_sending
+ @sending = true
+ @cv.broadcast
+ end
+ end
+
+ def sent!
+ synchronize do
+ @sent = true
+ @cv.broadcast
+ end
+ end
+
+ def sending?; synchronize { @sending }; end
+ def committed?; synchronize { @committed }; end
+ def sent?; synchronize { @sent }; end
+
+ # Sets the HTTP status code.
+ def status=(status)
+ @status = Rack::Utils.status_code(status)
+ end
+
+ # Sets the HTTP content type.
+ def content_type=(content_type)
+ @content_type = content_type.to_s
+ end
+
+ # The response code of the request.
+ def response_code
+ @status
+ end
+
+ # Returns a string to ensure compatibility with <tt>Net::HTTPResponse</tt>.
+ def code
+ @status.to_s
+ end
+
+ # Returns the corresponding message for the current HTTP status code:
+ #
+ # response.status = 200
+ # response.message # => "OK"
+ #
+ # response.status = 404
+ # response.message # => "Not Found"
+ #
+ def message
+ Rack::Utils::HTTP_STATUS_CODES[@status]
+ end
+ alias_method :status_message, :message
+
+ # Returns the content of the response as a string. This contains the contents
+ # of any calls to <tt>render</tt>.
+ def body
+ strings = []
+ each { |part| strings << part.to_s }
+ strings.join
+ end
+
+ EMPTY = " "
+
+ # Allows you to manually set or override the response body.
+ def body=(body)
+ @blank = true if body == EMPTY
+
+ if body.respond_to?(:to_path)
+ @stream = body
+ else
+ synchronize do
+ @stream = build_buffer self, munge_body_object(body)
+ end
+ end
+ end
+
+ def body_parts
+ parts = []
+ @stream.each { |x| parts << x }
+ parts
+ end
+
+ def set_cookie(key, value)
+ ::Rack::Utils.set_cookie_header!(header, key, value)
+ end
+
+ def delete_cookie(key, value={})
+ ::Rack::Utils.delete_cookie_header!(header, key, value)
+ end
+
+ # The location header we'll be responding with.
+ def location
+ headers[LOCATION]
+ end
+ alias_method :redirect_url, :location
+
+ # Sets the location header we'll be responding with.
+ def location=(url)
+ headers[LOCATION] = url
+ end
+
+ def close
+ stream.close if stream.respond_to?(:close)
+ end
+
+ def abort
+ if stream.respond_to?(:abort)
+ stream.abort
+ elsif stream.respond_to?(:close)
+ # `stream.close` should really be reserved for a close from the
+ # other direction, but we must fall back to it for
+ # compatibility.
+ stream.close
+ end
+ end
+
+ # Turns the Response into a Rack-compatible array of the status, headers,
+ # and body.
+ def to_a
+ rack_response @status, @header.to_hash
+ end
+ alias prepare! to_a
+ alias to_ary to_a
+
+ # Returns the response cookies, converted to a Hash of (name => value) pairs
+ #
+ # assert_equal 'AuthorOfNewPage', r.cookies['author']
+ def cookies
+ cookies = {}
+ if header = self[SET_COOKIE]
+ header = header.split("\n") if header.respond_to?(:to_str)
+ header.each do |cookie|
+ if pair = cookie.split(';').first
+ key, value = pair.split("=").map { |v| Rack::Utils.unescape(v) }
+ cookies[key] = value
+ end
+ end
+ end
+ cookies
+ end
+
+ def _status_code
+ @status
+ end
+ private
+
+ def before_committed
+ end
+
+ def before_sending
+ end
+
+ def merge_default_headers(original, default)
+ return original unless default.respond_to?(:merge)
+
+ default.merge(original)
+ end
+
+ def build_buffer(response, body)
+ Buffer.new response, body
+ end
+
+ def munge_body_object(body)
+ body.respond_to?(:each) ? body : [body]
+ end
+
+ def assign_default_content_type_and_charset!(headers)
+ return if headers[CONTENT_TYPE].present?
+
+ @content_type ||= Mime::HTML
+ @charset ||= self.class.default_charset unless @charset == false
+
+ type = @content_type.to_s.dup
+ type << "; charset=#{@charset}" if append_charset?
+
+ headers[CONTENT_TYPE] = type
+ end
+
+ def append_charset?
+ !@sending_file && @charset != false
+ end
+
+ class RackBody
+ def initialize(response)
+ @response = response
+ end
+
+ def each(*args, &block)
+ @response.each(*args, &block)
+ end
+
+ def close
+ # Rack "close" maps to Response#abort, and *not* Response#close
+ # (which is used when the controller's finished writing)
+ @response.abort
+ end
+
+ def body
+ @response.body
+ end
+
+ def respond_to?(method, include_private = false)
+ if method.to_s == 'to_path'
+ @response.stream.respond_to?(method)
+ else
+ super
+ end
+ end
+
+ def to_path
+ @response.stream.to_path
+ end
+ end
+
+ def rack_response(status, header)
+ assign_default_content_type_and_charset!(header)
+ handle_conditional_get!
+
+ header[SET_COOKIE] = header[SET_COOKIE].join("\n") if header[SET_COOKIE].respond_to?(:join)
+
+ if NO_CONTENT_CODES.include?(@status)
+ header.delete CONTENT_TYPE
+ [status, header, []]
+ else
+ [status, header, RackBody.new(self)]
+ end
+ end
+ end
+end