# 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, 103, 204, 205, 304] cattr_accessor :default_charset, default: "utf-8" cattr_accessor :default_headers def self.return_only_media_type_on_content_type=(*) ActiveSupport::Deprecation.warn( ".return_only_media_type_on_content_type= is deprecated with no replacement and will be removed in 7.0." ) end def self.return_only_media_type_on_content_type ActiveSupport::Deprecation.warn( ".return_only_media_type_on_content_type is deprecated with no replacement and will be removed in 7.0." ) end 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. def content_type super.presence end # Media type of response. def media_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 Net::HTTPResponse. 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 render. 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 CONTENT_TYPE_PARSER = / \A (?[^;\s]+\s*(?:;\s*(?:(?!charset)[^;\s])+)*)? (?:;\s*charset=(?"?)(?[^;\s]+)\k)? /x # :nodoc: def parse_content_type(content_type) if content_type && match = CONTENT_TYPE_PARSER.match(content_type) ContentTypeHeader.new(match[:mime_type], match[: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 || "" type = "#{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 media_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_sym == :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 ActiveSupport.run_load_hooks(:action_dispatch_response, Response) end