lib/patron/response.rb



module Patron

  # Represents the response from the HTTP server.
  class Response
    include ResponseDecoding
    
    # @return [String] the original URL used to perform the request (contains the final URL after redirects)
    attr_reader :url

    # @return [Integer] the HTTP status code of the final response after all the redirects
    attr_reader :status

    # @return [String] the complete status line (code and message)
    attr_reader :status_line

    # @return [Integer] how many redirects were followed when fulfilling this request
    attr_reader :redirect_count

    # @return [String, nil] the response body as a String encoded as `Encoding::BINARY` or
    #           or `nil` if the response was written directly to a file
    attr_reader :body
    
    # @return [Hash] the response headers. If there were multiple headers received for the same value
    #   (like "Cookie"), the header values will be within an Array under the key for the header, in order.
    attr_reader :headers

    # @return [String] the recognized name of the charset for the response. The name is not checked
    #    to be a valid charset name, just stored. To check the charset for validity, use #body_decodable?
    attr_reader :charset

    # Overridden so that the output is shorter and there is no response body printed
    def inspect
      # Avoid spamming the console with the header and body data
      "#<Patron::Response @status_line='#{@status_line}'>"
    end

    def initialize(url, status, redirect_count, raw_header_data, body, default_charset = nil)
      @url            = url.force_encoding(Encoding::ASCII) # the URL is always an ASCII subset, _always_.
      @status         = status
      @redirect_count = redirect_count
      @body           = body.force_encoding(Encoding::BINARY) if body

      header_data = decode_header_data(raw_header_data)
      parse_headers(header_data)
      @charset = charset_from_content_type
    end

    # Tells whether the HTTP response code is less than 400
    #
    # @return [Boolean]
    def ok?
      !error?
    end
    
    # Tells whether the HTTP response code is larger than 399
    #
    # @return [Boolean]
    def error?
      status >= 400
    end
    
    # Tells whether the response body can be decoded losslessly into the curren internal encoding
    #
    # @return [Boolean] true if the body is decodable, false if otherwise
    def body_decodable?
      return true if @body.nil?
      return true if decoded_body
    rescue HeaderCharsetInvalid, NonRepresentableBody
      false
    end

    # Returns the response body converted into the Ruby process internal encoding (the one set as `Encoding.default_internal`).
    # As the response gets returned, the response body is not assumed to be in any encoding whatsoever - it will be explicitly
    # set to `Encoding::BINARY` (as if you were reading a file in binary mode).
    #
    # When you call `decoded_body`, the method will
    # look at the `Content-Type` response header, and check if that header specified a charset. If it did, the method will then
    # check whether the specified charset is valid (whether it is possible to find a matching `Encoding` class in the VM).
    # Once that succeeds, the method will check whether the response body _is_ in the encoding that the server said it is.
    #
    # This might not be the case - you can, for instance, easily serve an HTML document with a UTF-8 header (with the header
    # being configured somewhere on the webserver level) and then have the actual HTML document override it with a
    # `meta` element  or `charset` containing an overriding charset. However, parsing the response body is outside of scope for
    # Patron, so if this situation happens (the server sets a charset in the header but this header does not match what the server
    # actually sends in the body) you will get an exception stating this is a problem.
    #
    # The next step is actually converting the body to the internal Ruby encoding. That stage may raise an exception as well, if
    # you are using an internal encoding which can't represent the response body faithfully. For example, if you run Ruby with
    # a CJK internal encoding, and the response you are trying to decode uses Greek characters and is UTF-8, you are going to
    # get an exception since it is impossible to coerce those characters to your internal encoding.
    #
    # @raise {Patron::HeaderCharsetInvalid} when the server supplied a wrong or incorrect charset, {Patron::NonRepresentableBody}
    #    when unable to decode the body into the current process encoding.
    # @return [String, nil]
    def decoded_body
      return unless @body
      @decoded_body ||= decode_body(true)
    end
    
    # Works the same as `decoded_body`, with one substantial difference: characters which can't be represented
    # in your process' default encoding are going to be replaced with question marks. This can be used for raising
    # errors when you receive responses which indicate errors on the server you are calling. For example, if you expect
    # a binary download, and the server sends you an error message and you don't really want to bother figuring out
    # the encoding it has - but you need to append this response to an error log or similar.
    #
    # @see Patron::Response#decoded_body
    # @return [String, nil]
    def inspectable_body
      return unless @body
      @inspectable_body ||= decode_body(false)
    end

    private

    # Called by the C code to parse and set the headers
    def parse_headers(header_data_for_multiple_responses)
      @headers = {}

      responses = Patron::HeaderParser.parse(header_data_for_multiple_responses)
      last_response = responses[-1] # Only use the last response (for proxies and redirects)

      @status_line = last_response.status_line
      last_response.headers.each do |line|
        hdr, val = line.split(":", 2)

        val.strip! unless val.nil?

        if @headers.key?(hdr)
          @headers[hdr] = [@headers[hdr]] unless @headers[hdr].kind_of? Array
          @headers[hdr] << val
        else
          @headers[hdr] = val
        end
      end
    end
  end
end