diff options
Diffstat (limited to 'actionpack/lib/action_dispatch/http/response.rb')
-rw-r--r-- | actionpack/lib/action_dispatch/http/response.rb | 520 |
1 files changed, 520 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..1d38942a31 --- /dev/null +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -0,0 +1,520 @@ +# frozen_string_literal: true + +require "active_support/core_ext/module/attribute_accessors" +require "action_dispatch/http/filter_redirect" +require "action_dispatch/http/cache" +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 + class Header < DelegateClass(Hash) # :nodoc: + def initialize(response, header) + @response = response + super(header) + end + + def []=(k, v) + if @response.sending? || @response.sent? + raise ActionDispatch::IllegalStateError, "header already sent" + end + + super + end + + def merge(other) + self.class.new @response, __getobj__.merge(other) + end + + def to_hash + __getobj__.dup + end + end + + # The request that the response is responding to. + attr_accessor :request + + # The HTTP status code. + attr_reader :status + + # Get headers for this response. + attr_reader :header + + alias_method :headers, :header + + delegate :[], :[]=, to: :@header + + def each(&block) + sending! + x = @stream.each(&block) + sent! + x + end + + CONTENT_TYPE = "Content-Type" + SET_COOKIE = "Set-Cookie" + LOCATION = "Location" + NO_CONTENT_CODES = [100, 101, 102, 204, 205, 304] + + cattr_accessor :default_charset, default: "utf-8" + cattr_accessor :default_headers + + include Rack::Response::Helpers + # Aliasing these off because AD::Http::Cache::Response defines them. + alias :_cache_control :cache_control + alias :_cache_control= :cache_control= + + include ActionDispatch::Http::FilterRedirect + include ActionDispatch::Http::Cache::Response + include MonitorMixin + + class Buffer # :nodoc: + def initialize(response, buf) + @response = response + @buf = buf + @closed = false + @str_body = nil + end + + def body + @str_body ||= begin + buf = +"" + each { |chunk| buf << chunk } + buf + end + end + + def write(string) + raise IOError, "closed stream" if closed? + + @str_body = nil + @response.commit! + @buf.push string + end + + def each(&block) + if @str_body + return enum_for(:each) unless block_given? + + yield @str_body + else + each_chunk(&block) + end + end + + def abort + end + + def close + @response.commit! + @closed = true + end + + def closed? + @closed + end + + private + + def each_chunk(&block) + @buf.each(&block) + end + end + + def self.create(status = 200, header = {}, body = [], default_headers: self.default_headers) + header = merge_default_headers(header, default_headers) + new status, header, body + end + + def self.merge_default_headers(original, default) + default.respond_to?(:merge) ? default.merge(original) : original + end + + # The underlying body, as a streamable object. + attr_reader :stream + + def initialize(status = 200, header = {}, body = []) + super() + + @header = Header.new(self, header) + + self.body, self.status = body, status + + @cv = new_cond + @committed = false + @sending = false + @sent = false + + prepare_cache_control! + + yield self if block_given? + end + + def has_header?(key); headers.key? key; end + def get_header(key); headers[key]; end + def set_header(key, v); headers[key] = v; end + def delete_header(key); headers.delete key; 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 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. + def content_type=(content_type) + return unless content_type + new_header_info = parse_content_type(content_type.to_s) + prev_header_info = parsed_content_type_header + charset = new_header_info.charset || prev_header_info.charset + charset ||= self.class.default_charset unless prev_header_info.mime_type + set_content_type new_header_info.mime_type, charset + end + + # Content type of response. + # It returns just MIME type and does NOT contain charset part. + def content_type + parsed_content_type_header.mime_type + end + + def sending_file=(v) + if true == v + self.charset = false + end + end + + # Sets the HTTP character set. In case of +nil+ parameter + # it sets the charset to +default_charset+. + # + # response.charset = 'utf-16' # => 'utf-16' + # response.charset = nil # => 'utf-8' + def charset=(charset) + content_type = parsed_content_type_header.mime_type + if false == charset + set_content_type content_type, nil + else + set_content_type content_type, charset || self.class.default_charset + end + end + + # 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. + def charset + header_info = parsed_content_type_header + header_info.charset || self.class.default_charset + 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 + @stream.body + end + + def write(string) + @stream.write string + end + + # Allows you to manually set or override the response body. + def body=(body) + if body.respond_to?(:to_path) + @stream = body + else + synchronize do + @stream = build_buffer self, munge_body_object(body) + end + end + end + + # Avoid having to pass an open file handle as the response body. + # Rack::Sendfile will usually intercept the response and uses + # the path directly, so there is no reason to open the file. + class FileBody #:nodoc: + attr_reader :to_path + + def initialize(path) + @to_path = path + end + + def body + File.binread(to_path) + end + + # Stream the file's contents if Rack::Sendfile isn't present. + def each + File.open(to_path, "rb") do |file| + while chunk = file.read(16384) + yield chunk + end + end + end + end + + # Send the file stored at +path+ as the response body. + def send_file(path) + commit! + @stream = FileBody.new(path) + end + + def reset_body! + @stream = build_buffer(self, []) + end + + def body_parts + parts = [] + @stream.each { |x| parts << x } + parts + end + + # The location header we'll be responding with. + alias_method :redirect_url, :location + + 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. Allows explicit splatting: + # + # status, headers, body = *response + def to_a + commit! + rack_response @status, @header.to_hash + end + alias prepare! 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 = get_header(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 + + private + + ContentTypeHeader = Struct.new :mime_type, :charset + NullContentTypeHeader = ContentTypeHeader.new nil, nil + + def parse_content_type(content_type) + if content_type + type, charset = content_type.split(/;\s*charset=/) + type = nil if type && type.empty? + ContentTypeHeader.new(type, charset) + else + NullContentTypeHeader + end + end + + # Small internal convenience method to get the parsed version of the current + # content type header. + def parsed_content_type_header + parse_content_type(get_header(CONTENT_TYPE)) + end + + def set_content_type(content_type, charset) + type = (content_type || "").dup + type << "; charset=#{charset.to_s.downcase}" if charset + set_header CONTENT_TYPE, type + end + + def before_committed + return if committed? + assign_default_content_type_and_charset! + merge_and_normalize_cache_control!(@cache_control) + handle_conditional_get! + handle_no_content! + end + + def before_sending + # Normally we've already committed by now, but it's possible + # (e.g., if the controller action tries to read back its own + # response) to get here before that. In that case, we must force + # an "early" commit: we're about to freeze the headers, so this is + # our last chance. + commit! unless committed? + + headers.freeze + request.commit_cookie_jar! unless committed? + 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! + return if content_type + + ct = parsed_content_type_header + set_content_type(ct.mime_type || Mime[:html].to_s, + ct.charset || self.class.default_charset) + 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 + + def to_ary + nil + end + end + + def handle_no_content! + if NO_CONTENT_CODES.include?(@status) + @header.delete CONTENT_TYPE + @header.delete "Content-Length" + end + end + + def rack_response(status, header) + if NO_CONTENT_CODES.include?(status) + [status, header, []] + else + [status, header, RackBody.new(self)] + end + end + end +end |