lib/httpx/response.rb in httpx-1.0.2 vs lib/httpx/response.rb in httpx-1.1.0
- old
+ new
@@ -5,62 +5,103 @@
require "tempfile"
require "fileutils"
require "forwardable"
module HTTPX
+ # Defines a HTTP response is handled internally, with a few properties exposed as attributes,
+ # implements (indirectly, via the +body+) the IO write protocol to internally buffer payloads,
+ # implements the IO reader protocol in order for users to buffer/stream it, acts as an enumerable
+ # (of payload chunks).
class Response
extend Forwardable
include Callbacks
- attr_reader :status, :headers, :body, :version
+ # the HTTP response status code
+ attr_reader :status
+ # an HTTPX::Headers object containing the response HTTP headers.
+ attr_reader :headers
+
+ # a HTTPX::Response::Body object wrapping the response body.
+ attr_reader :body
+
+ # The HTTP protocol version used to fetch the response.
+ attr_reader :version
+
+ # returns the response body buffered in a string.
def_delegator :@body, :to_s
def_delegator :@body, :to_str
+ # implements the IO reader +#read+ interface.
def_delegator :@body, :read
+ # copies the response body to a different location.
def_delegator :@body, :copy_to
+ # closes the body.
def_delegator :@body, :close
+ # the corresponding request uri.
def_delegator :@request, :uri
+ # the IP address of the peer server.
+ def_delegator :@request, :peer_address
+
+ # inits the instance with the corresponding +request+ to this response, an the
+ # response HTTP +status+, +version+ and HTTPX::Headers instance of +headers+.
def initialize(request, status, version, headers)
@request = request
@options = request.options
@version = version
@status = Integer(status)
@headers = @options.headers_class.new(headers)
@body = @options.response_body_class.new(self, @options)
@finished = complete?
+ @content_type = nil
end
+ # merges headers defined in +h+ into the response headers.
def merge_headers(h)
@headers = @headers.merge(h)
end
+ # writes +data+ chunk into the response body.
def <<(data)
@body.write(data)
end
+ # returns the response mime type, as per what's declared in the content-type header.
+ #
+ # response.content_type #=> "text/plain"
def content_type
@content_type ||= ContentType.new(@headers["content-type"])
end
+ # returns whether the response has been fully fetched.
def finished?
@finished
end
+ # marks the response as finished, freezes the headers.
def finish!
@finished = true
@headers.freeze
end
+ # returns whether the response contains body payload.
def bodyless?
@request.verb == "HEAD" ||
- no_data?
+ @status < 200 || # informational response
+ @status == 204 ||
+ @status == 205 ||
+ @status == 304 || begin
+ content_length = @headers["content-length"]
+ return false if content_length.nil?
+
+ content_length == "0"
+ end
end
def complete?
bodyless? || (@request.verb == "CONNECT" && @status == 200)
end
@@ -73,36 +114,57 @@
"@headers=#{@headers} " \
"@body=#{@body.bytesize}>"
end
# :nocov:
+ # returns an instance of HTTPX::HTTPError if the response has a 4xx or 5xx
+ # status code, or nothing.
+ #
+ # ok_response.error #=> nil
+ # not_found_response.error #=> HTTPX::HTTPError instance, status 404
def error
return if @status < 400
HTTPError.new(self)
end
+ # it raises the exception returned by +error+, or itself otherwise.
+ #
+ # ok_response.raise_for_status #=> ok_response
+ # not_found_response.raise_for_status #=> raises HTTPX::HTTPError exception
def raise_for_status
return self unless (err = error)
raise err
end
+ # decodes the response payload into a ruby object **if** the payload is valid json.
+ #
+ # response.json #≈> { "foo" => "bar" } for "{\"foo\":\"bar\"}" payload
+ # response.json(symbolize_names: true) #≈> { foo: "bar" } for "{\"foo\":\"bar\"}" payload
def json(*args)
decode(Transcoder::JSON, *args)
end
+ # decodes the response payload into a ruby object **if** the payload is valid
+ # "application/x-www-urlencoded" or "multipart/form-data".
def form
decode(Transcoder::Form)
end
+ # decodes the response payload into a Nokogiri::XML::Node object **if** the payload is valid
+ # "application/xml" (requires the "nokogiri" gem).
def xml
decode(Transcoder::Xml)
end
private
+ # decodes the response payload using the given +transcoder+, which implements the decoding logic.
+ #
+ # +transcoder+ must implement the internal transcoder API, i.e. respond to <tt>decode(HTTPX::Response response)</tt>,
+ # which returns a decoder which responds to <tt>call(HTTPX::Response response, **kwargs)</tt>
def decode(transcoder, *args)
# TODO: check if content-type is a valid format, i.e. "application/json" for json parsing
decoder = transcoder.decode(self)
@@ -110,74 +172,91 @@
@body.rewind
decoder.call(self, *args)
end
-
- def no_data?
- @status < 200 || # informational response
- @status == 204 ||
- @status == 205 ||
- @status == 304 || begin
- content_length = @headers["content-length"]
- return false if content_length.nil?
-
- content_length == "0"
- end
- end
end
+ # Helper class which decodes the HTTP "content-type" header.
class ContentType
MIME_TYPE_RE = %r{^([^/]+/[^;]+)(?:$|;)}.freeze
CHARSET_RE = /;\s*charset=([^;]+)/i.freeze
def initialize(header_value)
@header_value = header_value
end
+ # returns the mime type declared in the header.
+ #
+ # ContentType.new("application/json; charset=utf-8").mime_type #=> "application/json"
def mime_type
return @mime_type if defined?(@mime_type)
m = @header_value.to_s[MIME_TYPE_RE, 1]
m && @mime_type = m.strip.downcase
end
+ # returns the charset declared in the header.
+ #
+ # ContentType.new("application/json; charset=utf-8").charset #=> "utf-8"
+ # ContentType.new("text/plain").charset #=> nil
def charset
return @charset if defined?(@charset)
m = @header_value.to_s[CHARSET_RE, 1]
m && @charset = m.strip.delete('"')
end
end
+ # Wraps an error which has happened while processing an HTTP Request. It has partial
+ # public API parity with HTTPX::Response, so users should rely on it to infer whether
+ # the returned response is one or the other.
+ #
+ # response = HTTPX.get("https://some-domain/path") #=> response is HTTPX::Response or HTTPX::ErrorResponse
+ # response.raise_for_status #=> raises if it wraps an error
class ErrorResponse
include Loggable
extend Forwardable
- attr_reader :request, :response, :error
+ # the corresponding HTTPX::Request instance.
+ attr_reader :request
+ # the HTTPX::Response instance, when there is one (i.e. error happens fetching the response).
+ attr_reader :response
+
+ # the wrapped exception.
+ attr_reader :error
+
+ # the request uri
def_delegator :@request, :uri
+ # the IP address of the peer server.
+ def_delegator :@request, :peer_address
+
def initialize(request, error, options)
@request = request
@response = request.response if request.response.is_a?(Response)
@error = error
@options = Options.new(options)
log_exception(@error)
end
+ # returns the exception full message.
def to_s
@error.full_message(highlight: false)
end
+ # closes the error resources.
def close
- @response.close if @response.respond_to?(:close)
+ @response.close if @response && @response.respond_to?(:close)
end
+ # always true for error responses.
def finished?
true
end
+ # raises the wrapped exception.
def raise_for_status
raise @error
end
end
end