# 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: # = Action Dispatch \Response # # 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). # # The \Response object for the current request is exposed on controllers as # ActionController::Metal#response. ActionController::Metal also provides a # few additional methods that delegate to attributes of the \Response such as # ActionController::Metal#headers. # # Integration tests will likely also want to inspect responses in # more detail. Methods such as Integration::RequestHelpers#get # and Integration::RequestHelpers#post return instances of # TestResponse (which inherits from \Response) for this purpose. # # 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 begin # For `Rack::Headers` (Rack 3+): require "rack/headers" Headers = ::Rack::Headers rescue LoadError # For `Rack::Utils::HeaderHash`: require "rack/utils" Headers = ::Rack::Utils::HeaderHash end # To be deprecated: Header = Headers # The request that the response is responding to. attr_accessor :request # The HTTP status code. attr_reader :status # The headers for the response. # # header["Content-Type"] # => "text/plain" # header["Content-Type"] = "application/json" # header["Content-Type"] # => "application/json" # # Also aliased as +headers+. # # headers["Content-Type"] # => "text/plain" # headers["Content-Type"] = "application/json" # headers["Content-Type"] # => "application/json" # # Also aliased as +header+ for compatibility. attr_reader :headers alias_method :header, :headers delegate :[], :[]=, to: :@headers def each(&block) sending! x = @stream.each(&block) sent! x end CONTENT_TYPE = "Content-Type" SET_COOKIE = "Set-Cookie" NO_CONTENT_CODES = [100, 101, 102, 103, 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 to_ary @buf.respond_to?(:to_ary) ? @buf.to_ary : @buf.each 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 alias_method :<<, :write 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, headers = {}, body = [], default_headers: self.default_headers) headers = merge_default_headers(headers, default_headers) new status, headers, 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, headers = nil, body = []) super() @headers = Headers.new headers&.each do |key, value| @headers[key] = value end 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, @headers.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? @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 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 BODY_METHODS = { to_ary: true, each: true, call: true, to_path: true } def respond_to?(method, include_private = false) if BODY_METHODS.key?(method) @response.stream.respond_to?(method) else super end end def to_ary @response.stream.to_ary end def each(*args, &block) @response.each(*args, &block) end def call(*arguments, &block) @response.stream.call(*arguments, &block) end def to_path @response.stream.to_path end end def handle_no_content! if NO_CONTENT_CODES.include?(@status) @headers.delete CONTENT_TYPE @headers.delete "Content-Length" end end def rack_response(status, headers) if NO_CONTENT_CODES.include?(status) [status, headers, []] else [status, headers, RackBody.new(self)] end end end ActiveSupport.run_load_hooks(:action_dispatch_response, Response) end