rest-client--rest-client/lib/restclient/abstract_response.rb

303 lines
8.1 KiB
Ruby

require 'cgi'
require 'http-cookie'
module RestClient
# The AbstractResponse module provides common functionality for RestClient
# responses. It is included by {RestClient::Response} and
# {RestClient::RawResponse}.
#
# Classes that include AbstractResponse should call {response_set_vars} as
# part of their `#initialize` method.
#
# Ideally this would have been a parent class, not a module. But the original
# RestClient API made {Response} be a subclass of `String`, so we are
# stuck with that for backwards compatibility.
#
module AbstractResponse
attr_reader :net_http_res, :request, :start_time, :end_time, :duration
def inspect
raise NotImplementedError.new('must override in subclass')
end
# Logger from the request, potentially nil.
#
# @see Request#log
#
def log
request.log
end
# Write log information about the response.
#
# @return [void]
#
def log_response
return unless log
code = net_http_res.code
res_name = net_http_res.class.to_s.gsub(/\ANet::HTTP/, '')
content_type = (net_http_res['Content-type'] || '').gsub(/;.*\z/, '')
log << "# => #{code} #{res_name} | #{content_type} #{size} bytes, #{sprintf('%.2f', duration)}s\n"
end
# HTTP status code
#
# @return [Integer]
#
def code
@code ||= @net_http_res.code.to_i
end
# An array of prior responses in the redirection chain, if any. If
# RestClient followed any redirects, this provides a way to see each
# individual response in the chain.
#
# @return [Array<Response>]
#
# @see Request#redirection_history
#
def history
@history ||= request.redirection_history || []
end
# A hash of the headers, beautified with symbols and underscores.
# e.g. `"Content-type"` will become `:content_type`.
#
# @see beautify_headers
#
# @return [Hash]
#
def headers
@headers ||= AbstractResponse.beautify_headers(@net_http_res.to_hash)
end
# The raw headers.
#
# @return [Hash]
def raw_headers
@raw_headers ||= @net_http_res.to_hash
end
# @param [Net::HTTPResponse] net_http_res
# @param [RestClient::Request] request
# @param [Time] start_time
#
# @return [void]
#
def response_set_vars(net_http_res, request, start_time)
@net_http_res = net_http_res
@request = request
@start_time = start_time
@end_time = Time.now
if @start_time
@duration = @end_time - @start_time
else
@duration = nil
end
# prime redirection history
history
end
# Hash of cookies extracted from response headers.
#
# **Note:** This will return only cookies whose domain matches this
# request, and may not even return all of those cookies if there are
# duplicate names. Use the full cookie_jar for more nuanced access.
#
# @see #cookie_jar
#
# @return [Hash]
#
def cookies
hash = {}
cookie_jar.cookies(@request.uri).each do |cookie|
hash[cookie.name] = cookie.value
end
hash
end
# Cookie jar extracted from response headers.
#
# @return [HTTP::CookieJar]
#
def cookie_jar
return @cookie_jar if defined?(@cookie_jar) && @cookie_jar
jar = @request.cookie_jar.dup
headers.fetch(:set_cookie, []).each do |cookie|
jar.parse(cookie, @request.uri)
end
@cookie_jar = jar
end
# Return the default behavior corresponding to the response code:
#
# For 20x status codes: return the response itself
#
# For 30x status codes, if there is a `Location` header and we have not yet
# reached {Request#max_redirects}:
#
# - 301, 302, 307: redirect GET / HEAD requests
# - 303: redirect, changing method to GET
#
# For all other responses, raise a response exception, a subclass of
# {ExceptionWithResponse} corresponding to the HTTP status code.
#
# For example, HTTP 404 => {RestClient::NotFound RestClient::NotFound}
#
# @raise [ExceptionWithResponse] for non-20x status codes, the exception
# from {RestClient::Exceptions::EXCEPTIONS_MAP} based on
# {RestClient::STATUSES} will be thrown.
#
def return!(&block)
case code
when 200..207
self
when 301, 302, 307
case request.method
when 'get', 'head'
check_max_redirects
follow_redirection(&block)
else
raise exception_with_response
end
when 303
check_max_redirects
follow_get_redirection(&block)
else
raise exception_with_response
end
end
# @deprecated Use {code} instead.
def to_i
warn('warning: calling Response#to_i is deprecated. Use .code instead.')
super
end
# @return [String]
def description
"#{code} #{STATUSES[code]} | #{(headers[:content_type] || '').gsub(/;.*$/, '')} #{size} bytes\n"
end
# Follow a redirection response by making a new HTTP request to the
# redirection target.
def follow_redirection(&block)
_follow_redirection(request.args.dup, &block)
end
# Follow a redirection response, but change the HTTP method to GET and drop
# the payload from the original request.
def follow_get_redirection(&block)
new_args = request.args.dup
new_args[:method] = :get
new_args.delete(:payload)
_follow_redirection(new_args, &block)
end
# Convert headers hash into canonical form.
#
# Header names will be converted to lowercase symbols with underscores
# instead of hyphens.
#
# Headers specified multiple times will be joined by comma and space,
# except for Set-Cookie, which will always be an array.
#
# Per RFC 2616, if a server sends multiple headers with the same key, they
# MUST be able to be joined into a single header by a comma. However,
# Set-Cookie (RFC 6265) cannot because commas are valid within cookie
# definitions. The newer RFC 7230 notes (3.2.2) that Set-Cookie should be
# handled as a special case.
#
# http://tools.ietf.org/html/rfc2616#section-4.2
# http://tools.ietf.org/html/rfc7230#section-3.2.2
# http://tools.ietf.org/html/rfc6265
#
# @param headers [Hash]
# @return [Hash]
#
def self.beautify_headers(headers)
headers.inject({}) do |out, (key, value)|
key_sym = key.tr('-', '_').downcase.to_sym
# Handle Set-Cookie specially since it cannot be joined by comma.
if key.downcase == 'set-cookie'
out[key_sym] = value
else
out[key_sym] = value.join(', ')
end
out
end
end
private
# Follow a redirection
#
# @param new_args [Hash] Start with this hash of arguments for the
# redirection request. The hash will be mutated, so be sure to dup any
# existing hash that should not be modified.
#
def _follow_redirection(new_args, &block)
# parse location header and merge into existing URL
url = headers[:location]
# cannot follow redirection if there is no location header
unless url
raise exception_with_response
end
# handle relative redirects
unless url.start_with?('http')
url = URI.parse(request.url).merge(url).to_s
end
new_args[:url] = url
new_args[:password] = request.password
new_args[:user] = request.user
new_args[:headers] = request.headers
new_args[:max_redirects] = request.max_redirects - 1
# pass through our new cookie jar
new_args[:cookies] = cookie_jar
# prepare new request
new_req = Request.new(new_args)
# append self to redirection history
new_req.redirection_history = history + [self]
# execute redirected request
new_req.execute(&block)
end
def check_max_redirects
if request.max_redirects <= 0
raise exception_with_response
end
end
def exception_with_response
begin
klass = Exceptions::EXCEPTIONS_MAP.fetch(code)
rescue KeyError
raise RequestFailed.new(self, code)
end
raise klass.new(self, code)
end
end
end