mirror of
https://github.com/HoneyryderChuck/httpx.git
synced 2025-07-22 00:00:47 -04:00
Compare commits
5 Commits
61ce888e47
...
062109a5bc
Author | SHA1 | Date | |
---|---|---|---|
|
062109a5bc | ||
|
09a3df54c4 | ||
|
554b5a663c | ||
|
0cb169afab | ||
|
f089d57d7d |
@ -61,8 +61,12 @@ module HTTPX
|
||||
# Normalizes a _domain_ using the Punycode algorithm as necessary.
|
||||
# The result will be a downcased, ASCII-only string.
|
||||
def normalize(domain)
|
||||
domain = domain.chomp(".").unicode_normalize(:nfc) unless domain.ascii_only?
|
||||
Punycode.encode_hostname(domain).downcase
|
||||
unless domain.ascii_only?
|
||||
domain = domain.chomp(".").unicode_normalize(:nfc)
|
||||
domain = Punycode.encode_hostname(domain)
|
||||
end
|
||||
|
||||
domain.downcase
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1,20 +1,27 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module HTTPX
|
||||
# the default exception class for exceptions raised by HTTPX.
|
||||
class Error < StandardError; end
|
||||
|
||||
class UnsupportedSchemeError < Error; end
|
||||
|
||||
class ConnectionError < Error; end
|
||||
|
||||
# Error raised when there was a timeout. Its subclasses allow for finer-grained
|
||||
# control of which timeout happened.
|
||||
class TimeoutError < Error
|
||||
# The timeout value which caused this error to be raised.
|
||||
attr_reader :timeout
|
||||
|
||||
# initializes the timeout exception with the +timeout+ causing the error, and the
|
||||
# error +message+ for it.
|
||||
def initialize(timeout, message)
|
||||
@timeout = timeout
|
||||
super(message)
|
||||
end
|
||||
|
||||
# clones this error into a HTTPX::ConnectionTimeoutError.
|
||||
def to_connection_error
|
||||
ex = ConnectTimeoutError.new(@timeout, message)
|
||||
ex.set_backtrace(backtrace)
|
||||
@ -22,11 +29,19 @@ module HTTPX
|
||||
end
|
||||
end
|
||||
|
||||
# Error raised when there was a timeout establishing the connection to a server.
|
||||
# This may be raised due to timeouts during TCP and TLS (when applicable) connection
|
||||
# establishment.
|
||||
class ConnectTimeoutError < TimeoutError; end
|
||||
|
||||
# Error raised when there was a timeout while sending a request, or receiving a response
|
||||
# from the server.
|
||||
class RequestTimeoutError < TimeoutError
|
||||
# The HTTPX::Request request object this exception refers to.
|
||||
attr_reader :request
|
||||
|
||||
# initializes the exception with the +request+ and +response+ it refers to, and the
|
||||
# +timeout+ causing the error, and the
|
||||
def initialize(request, response, timeout)
|
||||
@request = request
|
||||
@response = response
|
||||
@ -38,19 +53,28 @@ module HTTPX
|
||||
end
|
||||
end
|
||||
|
||||
# Error raised when there was a timeout while receiving a response from the server.
|
||||
class ReadTimeoutError < RequestTimeoutError; end
|
||||
|
||||
# Error raised when there was a timeout while sending a request from the server.
|
||||
class WriteTimeoutError < RequestTimeoutError; end
|
||||
|
||||
# Error raised when there was a timeout while waiting for the HTTP/2 settings frame from the server.
|
||||
class SettingsTimeoutError < TimeoutError; end
|
||||
|
||||
# Error raised when there was a timeout while resolving a domain to an IP.
|
||||
class ResolveTimeoutError < TimeoutError; end
|
||||
|
||||
# Error raised when there was an error while resolving a domain to an IP.
|
||||
class ResolveError < Error; end
|
||||
|
||||
# Error raised when there was an error while resolving a domain to an IP
|
||||
# using a HTTPX::Resolver::Native resolver.
|
||||
class NativeResolveError < ResolveError
|
||||
attr_reader :connection, :host
|
||||
|
||||
# initializes the exception with the +connection+ it refers to, the +host+ domain
|
||||
# which failed to resolve, and the error +message+.
|
||||
def initialize(connection, host, message = "Can't resolve #{host}")
|
||||
@connection = connection
|
||||
@host = host
|
||||
@ -58,18 +82,26 @@ module HTTPX
|
||||
end
|
||||
end
|
||||
|
||||
# The exception class for HTTP responses with 4xx or 5xx status.
|
||||
class HTTPError < Error
|
||||
# The HTTPX::Response response object this exception refers to.
|
||||
attr_reader :response
|
||||
|
||||
# Creates the instance and assigns the HTTPX::Response +response+.
|
||||
def initialize(response)
|
||||
@response = response
|
||||
super("HTTP Error: #{@response.status} #{@response.headers}\n#{@response.body}")
|
||||
end
|
||||
|
||||
# The HTTP response status.
|
||||
#
|
||||
# error.status #=> 404
|
||||
def status
|
||||
@response.status
|
||||
end
|
||||
end
|
||||
|
||||
# error raised when a request was sent a server which can't reproduce a response, and
|
||||
# has therefore returned an HTTP response using the 421 status code.
|
||||
class MisdirectedRequestError < HTTPError; end
|
||||
end
|
||||
|
@ -3,6 +3,8 @@
|
||||
require "socket"
|
||||
|
||||
module HTTPX
|
||||
# Contains a set of options which are passed and shared across from session to its requests or
|
||||
# responses.
|
||||
class Options
|
||||
BUFFER_SIZE = 1 << 14
|
||||
WINDOW_SIZE = 1 << 14 # 16K
|
||||
@ -81,6 +83,50 @@ module HTTPX
|
||||
end
|
||||
end
|
||||
|
||||
# creates a new options instance from a given hash, which optionally define the following:
|
||||
#
|
||||
# :debug :: an object which log messages are written to (must respond to <tt><<</tt>)
|
||||
# :debug_level :: the log level of messages (can be 1, 2, or 3).
|
||||
# :ssl :: a hash of options which can be set as params of OpenSSL::SSL::SSLContext (see HTTPX::IO::SSL)
|
||||
# :http2_settings :: a hash of options to be passed to a HTTP2Next::Connection (ex: <tt>{ max_concurrent_streams: 2 }</tt>)
|
||||
# :fallback_protocol :: version of HTTP protocol to use by default in the absence of protocol negotiation
|
||||
# like ALPN (defaults to <tt>"http/1.1"</tt>)
|
||||
# :supported_compression_formats :: list of compressions supported by the transcoder layer (defaults to <tt>%w[gzip deflate]</tt>).
|
||||
# :decompress_response_body :: whether to auto-decompress response body (defaults to <tt>true</tt>).
|
||||
# :compress_request_body :: whether to auto-decompress response body (defaults to <tt>true</tt>)
|
||||
# :timeout :: hash of timeout configurations (supports <tt>:connect_timeout</tt>, <tt>:settings_timeout</tt>,
|
||||
# <tt>:operation_timeout</tt>, <tt>:keep_alive_timeout</tt>, <tt>:read_timeout</tt>, <tt>:write_timeout</tt>
|
||||
# and <tt>:request_timeout</tt>
|
||||
# :headers :: hash of HTTP headers (ex: <tt>{ "x-custom-foo" => "bar" }</tt>)
|
||||
# :window_size :: number of bytes to read from a socket
|
||||
# :buffer_size :: internal read and write buffer size in bytes
|
||||
# :body_threshold_size :: maximum size in bytes of response payload that is buffered in memory.
|
||||
# :request_class :: class used to instantiate a request
|
||||
# :response_class :: class used to instantiate a response
|
||||
# :headers_class :: class used to instantiate headers
|
||||
# :request_body_class :: class used to instantiate a request body
|
||||
# :response_body_class :: class used to instantiate a response body
|
||||
# :connection_class :: class used to instantiate connections
|
||||
# :options_class :: class used to instantiate options
|
||||
# :transport :: type of transport to use (set to "unix" for UNIX sockets)
|
||||
# :addresses :: bucket of peer addresses (can be a list of IP addresses, a hash of domain to list of adddresses;
|
||||
# paths should be used for UNIX sockets instead)
|
||||
# :io :: open socket, or domain/ip-to-socket hash, which requests should be sent to
|
||||
# :persistent :: whether to persist connections in between requests (defaults to <tt>true</tt>)
|
||||
# :resolver_class :: which resolver to use (defaults to <tt>:native</tt>, can also be <tt>:system<tt> for
|
||||
# using getaddrinfo or <tt>:https</tt> for DoH resolver, or a custom class)
|
||||
# :resolver_options :: hash of options passed to the resolver
|
||||
# :ip_families :: which socket families are supported (system-dependent)
|
||||
# :origin :: HTTP origin to set on requests with relative path (ex: "https://api.serv.com")
|
||||
# :base_path :: path to prefix given relative paths with (ex: "/v2")
|
||||
# :max_concurrent_requests :: max number of requests which can be set concurrently
|
||||
# :max_requests :: max number of requests which can be made on socket before it reconnects.
|
||||
# :params :: hash or array of key-values which will be encoded and set in the query string of request uris.
|
||||
# :form :: hash of array of key-values which will be form-or-multipart-encoded in requests body payload.
|
||||
# :json :: hash of array of key-values which will be JSON-encoded in requests body payload.
|
||||
# :xml :: Nokogiri XML nodes which will be encoded in requests body payload.
|
||||
#
|
||||
# This list of options are enhanced with each loaded plugin, see the plugin docs for details.
|
||||
def initialize(options = {})
|
||||
do_initialize(options)
|
||||
freeze
|
||||
|
@ -4,20 +4,45 @@ require "delegate"
|
||||
require "forwardable"
|
||||
|
||||
module HTTPX
|
||||
# Defines how an HTTP request is handled internally, both in terms of making attributes accessible,
|
||||
# as well as maintaining the state machine which manages streaming the request onto the wire.
|
||||
class Request
|
||||
extend Forwardable
|
||||
include Callbacks
|
||||
using URIExtensions
|
||||
|
||||
# default value used for "user-agent" header, when not overridden.
|
||||
USER_AGENT = "httpx.rb/#{VERSION}"
|
||||
|
||||
attr_reader :verb, :uri, :headers, :body, :state, :options, :response
|
||||
# the upcased string HTTP verb for this request.
|
||||
attr_reader :verb
|
||||
|
||||
# Exception raised during enumerable body writes
|
||||
# the absolute URI object for this request.
|
||||
attr_reader :uri
|
||||
|
||||
# an HTTPX::Headers object containing the request HTTP headers.
|
||||
attr_reader :headers
|
||||
|
||||
# an HTTPX::Request::Body object containing the request body payload (or +nil+, whenn there is none).
|
||||
attr_reader :body
|
||||
|
||||
# a symbol describing which frame is currently being flushed.
|
||||
attr_reader :state
|
||||
|
||||
# an HTTPX::Options object containing request options.
|
||||
attr_reader :options
|
||||
|
||||
# the corresponding HTTPX::Response object, when there is one.
|
||||
attr_reader :response
|
||||
|
||||
# Exception raised during enumerable body writes.
|
||||
attr_reader :drain_error
|
||||
|
||||
# will be +true+ when request body has been completely flushed.
|
||||
def_delegator :@body, :empty?
|
||||
|
||||
# initializes the instance with the given +verb+, an absolute or relative +uri+, and the
|
||||
# request options.
|
||||
def initialize(verb, uri, options = {})
|
||||
@verb = verb.to_s.upcase
|
||||
@options = Options.new(options)
|
||||
@ -37,16 +62,20 @@ module HTTPX
|
||||
|
||||
@body = @options.request_body_class.new(@headers, @options)
|
||||
@state = :idle
|
||||
@response = nil
|
||||
end
|
||||
|
||||
# the read timeout defied for this requet.
|
||||
def read_timeout
|
||||
@options.timeout[:read_timeout]
|
||||
end
|
||||
|
||||
# the write timeout defied for this requet.
|
||||
def write_timeout
|
||||
@options.timeout[:write_timeout]
|
||||
end
|
||||
|
||||
# the request timeout defied for this requet.
|
||||
def request_timeout
|
||||
@options.timeout[:request_timeout]
|
||||
end
|
||||
@ -59,6 +88,7 @@ module HTTPX
|
||||
@trailers ||= @options.headers_class.new
|
||||
end
|
||||
|
||||
# returns +:r+ or +:w+, depending on whether the request is waiting for a response or flushing.
|
||||
def interests
|
||||
return :r if @state == :done || @state == :expect
|
||||
|
||||
@ -69,10 +99,12 @@ module HTTPX
|
||||
@headers = @headers.merge(h)
|
||||
end
|
||||
|
||||
# the URI scheme of the request +uri+.
|
||||
def scheme
|
||||
@uri.scheme
|
||||
end
|
||||
|
||||
# sets the +response+ on this request.
|
||||
def response=(response)
|
||||
return unless response
|
||||
|
||||
@ -85,6 +117,7 @@ module HTTPX
|
||||
emit(:response_started, response)
|
||||
end
|
||||
|
||||
# returnns the URI path of the request +uri+.
|
||||
def path
|
||||
path = uri.path.dup
|
||||
path = +"" if path.nil?
|
||||
@ -93,16 +126,28 @@ module HTTPX
|
||||
path
|
||||
end
|
||||
|
||||
# https://bugs.ruby-lang.org/issues/15278
|
||||
# returs the URI authority of the request.
|
||||
#
|
||||
# session.build_request("GET", "https://google.com/query").authority #=> "google.com"
|
||||
# session.build_request("GET", "http://internal:3182/a").authority #=> "internal:3182"
|
||||
def authority
|
||||
@uri.authority
|
||||
end
|
||||
|
||||
# https://bugs.ruby-lang.org/issues/15278
|
||||
# returs the URI origin of the request.
|
||||
#
|
||||
# session.build_request("GET", "https://google.com/query").authority #=> "https://google.com"
|
||||
# session.build_request("GET", "http://internal:3182/a").authority #=> "http://internal:3182"
|
||||
def origin
|
||||
@uri.origin
|
||||
end
|
||||
|
||||
# returs the URI query string of the request (when available).
|
||||
#
|
||||
# session.build_request("GET", "https://search.com").query #=> ""
|
||||
# session.build_request("GET", "https://search.com?q=a").query #=> "q=a"
|
||||
# session.build_request("GET", "https://search.com", params: { q: "a"}).query #=> "q=a"
|
||||
# session.build_request("GET", "https://search.com?q=a", params: { foo: "bar"}).query #=> "q=a&foo&bar"
|
||||
def query
|
||||
return @query if defined?(@query)
|
||||
|
||||
@ -114,6 +159,7 @@ module HTTPX
|
||||
@query = query.join("&")
|
||||
end
|
||||
|
||||
# consumes and returns the next available chunk of request body that can be sent
|
||||
def drain_body
|
||||
return nil if @body.nil?
|
||||
|
||||
@ -139,6 +185,7 @@ module HTTPX
|
||||
end
|
||||
# :nocov:
|
||||
|
||||
# moves on to the +nextstate+ of the request state machine (when all preconditions are met)
|
||||
def transition(nextstate)
|
||||
case nextstate
|
||||
when :idle
|
||||
@ -173,6 +220,7 @@ module HTTPX
|
||||
nil
|
||||
end
|
||||
|
||||
# whether the request supports the 100-continue handshake and already processed the 100 response.
|
||||
def expects?
|
||||
@headers["expect"] == "100-continue" && @informational_status == 100 && !@response
|
||||
end
|
||||
|
@ -1,6 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module HTTPX
|
||||
# Implementation of the HTTP Request body as a delegator which iterates (responds to +each+) payload chunks.
|
||||
class Request::Body < SimpleDelegator
|
||||
class << self
|
||||
def new(_, options)
|
||||
@ -12,11 +13,12 @@ module HTTPX
|
||||
|
||||
attr_reader :threshold_size
|
||||
|
||||
# inits the instance with the request +headers+ and +options+, which contain the payload definition.
|
||||
def initialize(headers, options)
|
||||
@headers = headers
|
||||
@threshold_size = options.body_threshold_size
|
||||
|
||||
# forego compression in the Range cases
|
||||
# forego compression in the Range request case
|
||||
if @headers.key?("range")
|
||||
@headers.delete("accept-encoding")
|
||||
else
|
||||
@ -32,6 +34,7 @@ module HTTPX
|
||||
super(@body)
|
||||
end
|
||||
|
||||
# consumes and yields the request payload in chunks.
|
||||
def each(&block)
|
||||
return enum_for(__method__) unless block
|
||||
return if @body.nil?
|
||||
@ -46,12 +49,14 @@ module HTTPX
|
||||
end
|
||||
end
|
||||
|
||||
# if the +@body+ is rewindable, it rewinnds it.
|
||||
def rewind
|
||||
return if empty?
|
||||
|
||||
@body.rewind if @body.respond_to?(:rewind)
|
||||
end
|
||||
|
||||
# return +true+ if the +body+ has been fully drained (or does nnot exist).
|
||||
def empty?
|
||||
return true if @body.nil?
|
||||
return false if chunked?
|
||||
@ -59,28 +64,33 @@ module HTTPX
|
||||
@body.bytesize.zero?
|
||||
end
|
||||
|
||||
# returns the +@body+ payload size in bytes.
|
||||
def bytesize
|
||||
return 0 if @body.nil?
|
||||
|
||||
@body.bytesize
|
||||
end
|
||||
|
||||
# sets the body to yield using chunked trannsfer encoding format.
|
||||
def stream(body)
|
||||
encoded = body
|
||||
encoded = Transcoder::Chunker.encode(body.enum_for(:each)) if chunked?
|
||||
encoded
|
||||
end
|
||||
|
||||
# returns whether the body yields infinitely.
|
||||
def unbounded_body?
|
||||
return @unbounded_body if defined?(@unbounded_body)
|
||||
|
||||
@unbounded_body = !@body.nil? && (chunked? || @body.bytesize == Float::INFINITY)
|
||||
end
|
||||
|
||||
# returns whether the chunked transfer encoding header is set.
|
||||
def chunked?
|
||||
@headers["transfer-encoding"] == "chunked"
|
||||
end
|
||||
|
||||
# sets the chunked transfer encoding header.
|
||||
def chunk!
|
||||
@headers.add("transfer-encoding", "chunked")
|
||||
end
|
||||
@ -94,6 +104,13 @@ module HTTPX
|
||||
|
||||
private
|
||||
|
||||
# wraps the given body with the appropriate encoder.
|
||||
#
|
||||
# ..., json: { foo: "bar" }) #=> json encoder
|
||||
# ..., form: { foo: "bar" }) #=> form urlencoded encoder
|
||||
# ..., form: { foo: Pathname.open("path/to/file") }) #=> multipart urlencoded encoder
|
||||
# ..., form: { foo: File.open("path/to/file") }) #=> multipart urlencoded encoder
|
||||
# ..., form: { body: "bla") }) #=> raw data encoder
|
||||
def initialize_body(options)
|
||||
@body = if options.body
|
||||
Transcoder::Body.encode(options.body)
|
||||
@ -117,6 +134,7 @@ module HTTPX
|
||||
end
|
||||
|
||||
class << self
|
||||
# returns the +body+ wrapped with the correct deflater accordinng to the given +encodisng+.
|
||||
def initialize_deflater_body(body, encoding)
|
||||
case encoding
|
||||
when "gzip"
|
||||
@ -132,11 +150,13 @@ module HTTPX
|
||||
end
|
||||
end
|
||||
|
||||
# Wrapper yielder which can be used with functions which expect an IO writer.
|
||||
class ProcIO
|
||||
def initialize(block)
|
||||
@block = block
|
||||
end
|
||||
|
||||
# Implementation the IO write protocol, which yield the given chunk to +@block+.
|
||||
def write(data)
|
||||
@block.call(data.dup)
|
||||
data.bytesize
|
||||
|
@ -30,7 +30,7 @@ module HTTPX
|
||||
nameserver = nameserver[family] if nameserver.is_a?(Hash)
|
||||
Array(nameserver)
|
||||
end
|
||||
@ndots = @resolver_options[:ndots]
|
||||
@ndots = @resolver_options.fetch(:ndots, 1)
|
||||
@search = Array(@resolver_options[:search]).map { |srch| srch.scan(/[^.]+/) }
|
||||
@_timeouts = Array(@resolver_options[:timeouts])
|
||||
@timeouts = Hash.new { |timeouts, host| timeouts[host] = @_timeouts.dup }
|
||||
|
@ -7,24 +7,45 @@ 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
|
||||
|
||||
# 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
|
||||
@ -33,32 +54,49 @@ module HTTPX
|
||||
@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?
|
||||
@ -75,32 +113,53 @@ module HTTPX
|
||||
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
|
||||
|
||||
@ -112,20 +171,9 @@ module HTTPX
|
||||
|
||||
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
|
||||
@ -134,6 +182,9 @@ module HTTPX
|
||||
@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)
|
||||
|
||||
@ -141,6 +192,10 @@ module HTTPX
|
||||
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)
|
||||
|
||||
@ -149,12 +204,26 @@ module HTTPX
|
||||
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
|
||||
|
||||
def initialize(request, error, options)
|
||||
@ -165,18 +234,22 @@ module HTTPX
|
||||
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
|
||||
|
@ -1,9 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module HTTPX
|
||||
# Implementation of the HTTP Response body as a buffer which implements the IO writer protocol
|
||||
# (for buffering the response payload), the IO reader protocol (for consuming the response payload),
|
||||
# and can be iterated over (via #each, which yields the payload in chunks).
|
||||
class Response::Body
|
||||
attr_reader :encoding, :encodings
|
||||
# the payload encoding (i.e. "utf-8", "ASCII-8BIT")
|
||||
attr_reader :encoding
|
||||
|
||||
# Array of encodings contained in the response "content-encoding" header.
|
||||
attr_reader :encodings
|
||||
|
||||
# initialized with the corresponding HTTPX::Response +response+ and HTTPX::Options +options+.
|
||||
def initialize(response, options)
|
||||
@response = response
|
||||
@headers = response.headers
|
||||
@ -28,6 +36,8 @@ module HTTPX
|
||||
@state == :closed
|
||||
end
|
||||
|
||||
# write the response payload +chunk+ into the buffer. Inflates the chunk when required
|
||||
# and supported.
|
||||
def write(chunk)
|
||||
return if @state == :closed
|
||||
|
||||
@ -44,6 +54,7 @@ module HTTPX
|
||||
size
|
||||
end
|
||||
|
||||
# reads a chunk from the payload (implementation of the IO reader protocol).
|
||||
def read(*args)
|
||||
return unless @buffer
|
||||
|
||||
@ -55,10 +66,13 @@ module HTTPX
|
||||
@reader.read(*args)
|
||||
end
|
||||
|
||||
# size of the decoded response payload. May differ from "content-length" header if
|
||||
# response was encoded over-the-wire.
|
||||
def bytesize
|
||||
@length
|
||||
end
|
||||
|
||||
# yields the payload in chunks.
|
||||
def each
|
||||
return enum_for(__method__) unless block_given?
|
||||
|
||||
@ -74,12 +88,14 @@ module HTTPX
|
||||
end
|
||||
end
|
||||
|
||||
# returns the declared filename in the "contennt-disposition" header, when present.
|
||||
def filename
|
||||
return unless @headers.key?("content-disposition")
|
||||
|
||||
Utils.get_filename(@headers["content-disposition"])
|
||||
end
|
||||
|
||||
# returns the full response payload as a string.
|
||||
def to_s
|
||||
return "".b unless @buffer
|
||||
|
||||
@ -88,10 +104,16 @@ module HTTPX
|
||||
|
||||
alias_method :to_str, :to_s
|
||||
|
||||
# whether the payload is empty.
|
||||
def empty?
|
||||
@length.zero?
|
||||
end
|
||||
|
||||
# copies the payload to +dest+.
|
||||
#
|
||||
# body.copy_to("path/to/file")
|
||||
# body.copy_to(Pathname.new("path/to/file"))
|
||||
# body.copy_to(File.new("path/to/file"))
|
||||
def copy_to(dest)
|
||||
return unless @buffer
|
||||
|
||||
@ -132,6 +154,7 @@ module HTTPX
|
||||
end
|
||||
# :nocov:
|
||||
|
||||
# rewinds the response payload buffer.
|
||||
def rewind
|
||||
return unless @buffer
|
||||
|
||||
@ -179,7 +202,7 @@ module HTTPX
|
||||
@state = nextstate
|
||||
end
|
||||
|
||||
def _with_same_buffer_pos
|
||||
def _with_same_buffer_pos # :nodoc:
|
||||
return yield unless @buffer && @buffer.respond_to?(:pos)
|
||||
|
||||
# @type ivar @buffer: StringIO | Tempfile
|
||||
@ -193,7 +216,7 @@ module HTTPX
|
||||
end
|
||||
|
||||
class << self
|
||||
def initialize_inflater_by_encoding(encoding, response, **kwargs)
|
||||
def initialize_inflater_by_encoding(encoding, response, **kwargs) # :nodoc:
|
||||
case encoding
|
||||
when "gzip"
|
||||
Transcoder::GZIP.decode(response, **kwargs)
|
||||
|
@ -5,7 +5,10 @@ require "stringio"
|
||||
require "tempfile"
|
||||
|
||||
module HTTPX
|
||||
# wraps and delegates to an internal buffer, which can be a StringIO or a Tempfile.
|
||||
class Response::Buffer < SimpleDelegator
|
||||
# initializes buffer with the +threshold_size+ over which the payload gets buffer to a tempfile,
|
||||
# the initial +bytesize+, and the +encoding+.
|
||||
def initialize(threshold_size:, bytesize: 0, encoding: Encoding::BINARY)
|
||||
@threshold_size = threshold_size
|
||||
@bytesize = bytesize
|
||||
@ -20,16 +23,19 @@ module HTTPX
|
||||
@buffer = other.instance_variable_get(:@buffer).dup
|
||||
end
|
||||
|
||||
# size in bytes of the buffered content.
|
||||
def size
|
||||
@bytesize
|
||||
end
|
||||
|
||||
# writes the +chunk+ into the buffer.
|
||||
def write(chunk)
|
||||
@bytesize += chunk.bytesize
|
||||
try_upgrade_buffer
|
||||
@buffer.write(chunk)
|
||||
end
|
||||
|
||||
# returns the buffered content as a string.
|
||||
def to_s
|
||||
case @buffer
|
||||
when StringIO
|
||||
@ -49,6 +55,7 @@ module HTTPX
|
||||
end
|
||||
end
|
||||
|
||||
# closes the buffer.
|
||||
def close
|
||||
@buffer.close
|
||||
@buffer.unlink if @buffer.respond_to?(:unlink)
|
||||
@ -56,6 +63,8 @@ module HTTPX
|
||||
|
||||
private
|
||||
|
||||
# initializes the buffer into a StringIO, or turns it into a Tempfile when the threshold
|
||||
# has been reached.
|
||||
def try_upgrade_buffer
|
||||
if !@buffer.is_a?(Tempfile) && @bytesize > @threshold_size
|
||||
aux = @buffer
|
||||
@ -77,7 +86,7 @@ module HTTPX
|
||||
__setobj__(@buffer)
|
||||
end
|
||||
|
||||
def _with_same_buffer_pos
|
||||
def _with_same_buffer_pos # :nodoc:
|
||||
current_pos = @buffer.pos
|
||||
@buffer.rewind
|
||||
begin
|
||||
|
@ -1,6 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module HTTPX
|
||||
# Class implementing the APIs being used publicly.
|
||||
#
|
||||
# HTTPX.get(..) #=> delegating to an internal HTTPX::Session object.
|
||||
# HTTPX.plugin(..).get(..) #=> creating an intermediate HTTPX::Session with plugin, then sending the GET request
|
||||
class Session
|
||||
include Loggable
|
||||
include Chainable
|
||||
@ -8,6 +12,10 @@ module HTTPX
|
||||
|
||||
EMPTY_HASH = {}.freeze
|
||||
|
||||
# initializes the session with a set of +options+, which will be shared by all
|
||||
# requests sent from it.
|
||||
#
|
||||
# When pass a block, it'll yield itself to it, then closes after the block is evaluated.
|
||||
def initialize(options = EMPTY_HASH, &blk)
|
||||
@options = self.class.default_options.merge(options)
|
||||
@responses = {}
|
||||
@ -15,6 +23,11 @@ module HTTPX
|
||||
wrap(&blk) if blk
|
||||
end
|
||||
|
||||
# Yields itself the block, then closes it after the block is evaluated.
|
||||
#
|
||||
# session.wrap do |http|
|
||||
# http.get("https://wikipedia.com")
|
||||
# end # wikipedia connection closes here
|
||||
def wrap
|
||||
begin
|
||||
prev_persistent = @persistent
|
||||
@ -26,10 +39,31 @@ module HTTPX
|
||||
end
|
||||
end
|
||||
|
||||
# closes all the active connections from the session
|
||||
def close(*args)
|
||||
pool.close(*args)
|
||||
end
|
||||
|
||||
# performs one, or multple requests; it accepts:
|
||||
#
|
||||
# 1. one or multiple HTTPX::Request objects;
|
||||
# 2. an HTTP verb, then a sequence of URIs or URI/options tuples;
|
||||
# 3. one or multiple HTTP verb / uri / (optional) options tuples;
|
||||
#
|
||||
# when present, the set of +options+ kwargs is applied to all of the
|
||||
# sent requests.
|
||||
#
|
||||
# respectively returns a single HTTPX::Response response, or all of them in an Array, in the same order.
|
||||
#
|
||||
# resp1 = session.request(req1)
|
||||
# resp1, resp2 = session.request(req1, req2)
|
||||
# resp1 = session.request("GET", "https://server.org/a")
|
||||
# resp1, resp2 = session.request("GET", ["https://server.org/a", "https://server.org/b"])
|
||||
# resp1, resp2 = session.request(["GET", "https://server.org/a"], ["GET", "https://server.org/b"])
|
||||
# resp1 = session.request("POST", "https://server.org/a", form: { "foo" => "bar" })
|
||||
# resp1, resp2 = session.request(["POST", "https://server.org/a", form: { "foo" => "bar" }], ["GET", "https://server.org/b"])
|
||||
# resp1, resp2 = session.request("GET", ["https://server.org/a", "https://server.org/b"], headers: { "x-api-token" => "TOKEN" })
|
||||
#
|
||||
def request(*args, **options)
|
||||
raise ArgumentError, "must perform at least one request" if args.empty?
|
||||
|
||||
@ -40,6 +74,12 @@ module HTTPX
|
||||
responses
|
||||
end
|
||||
|
||||
# returns a HTTP::Request instance built from the HTTP +verb+, the request +uri+, and
|
||||
# the optional set of request-specific +options+. This request **must** be sent through
|
||||
# the same session it was built from.
|
||||
#
|
||||
# req = session.build_request("GET", "https://server.com")
|
||||
# resp = session.request(req)
|
||||
def build_request(verb, uri, options = EMPTY_HASH)
|
||||
rklass = @options.request_class
|
||||
options = @options.merge(options) unless options.is_a?(Options)
|
||||
@ -76,23 +116,29 @@ module HTTPX
|
||||
|
||||
private
|
||||
|
||||
# returns the HTTPX::Pool object which manages the networking required to
|
||||
# perform requests.
|
||||
def pool
|
||||
Thread.current[:httpx_connection_pool] ||= Pool.new
|
||||
end
|
||||
|
||||
# callback executed when a response for a given request has been received.
|
||||
def on_response(request, response)
|
||||
@responses[request] = response
|
||||
end
|
||||
|
||||
# callback executed when an HTTP/2 promise frame has been received.
|
||||
def on_promise(_, stream)
|
||||
log(level: 2) { "#{stream.id}: refusing stream!" }
|
||||
stream.refuse
|
||||
end
|
||||
|
||||
# returns the corresponding HTTP::Response to the given +request+ if it has been received.
|
||||
def fetch_response(request, _, _)
|
||||
@responses.delete(request)
|
||||
end
|
||||
|
||||
# returns the HTTPX::Connection through which the +request+ should be sent through.
|
||||
def find_connection(request, connections, options)
|
||||
uri = request.uri
|
||||
|
||||
@ -104,6 +150,8 @@ module HTTPX
|
||||
connection
|
||||
end
|
||||
|
||||
# sets the callbacks on the +connection+ required to process certain specific
|
||||
# connection lifecycle events which deal with request rerouting.
|
||||
def set_connection_callbacks(connection, connections, options)
|
||||
connection.only(:misdirected) do |misdirected_request|
|
||||
other_connection = connection.create_idle(ssl: { alpn_protocols: %w[http/1.1] })
|
||||
@ -131,6 +179,7 @@ module HTTPX
|
||||
end
|
||||
end
|
||||
|
||||
# returns an HTTPX::Connection for the negotiated Alternative Service (or none).
|
||||
def build_altsvc_connection(existing_connection, connections, alt_origin, origin, alt_params, options)
|
||||
# do not allow security downgrades on altsvc negotiation
|
||||
return if existing_connection.origin.scheme == "https" && alt_origin.scheme != "https"
|
||||
@ -166,6 +215,7 @@ module HTTPX
|
||||
nil
|
||||
end
|
||||
|
||||
# returns a set of HTTPX::Request objects built from the given +args+ and +options+.
|
||||
def build_requests(*args, options)
|
||||
request_options = @options.merge(options)
|
||||
|
||||
@ -189,6 +239,7 @@ module HTTPX
|
||||
requests
|
||||
end
|
||||
|
||||
# returns a new HTTPX::Connection object for the given +uri+ and set of +options+.
|
||||
def build_connection(uri, options)
|
||||
type = options.transport || begin
|
||||
case uri.scheme
|
||||
@ -216,11 +267,13 @@ module HTTPX
|
||||
end
|
||||
end
|
||||
|
||||
# sends an array of HTTPX::Request +requests+, returns the respective array of HTTPX::Response objects.
|
||||
def send_requests(*requests)
|
||||
connections = _send_requests(requests)
|
||||
receive_requests(requests, connections)
|
||||
end
|
||||
|
||||
# sends an array of HTTPX::Request objects
|
||||
def _send_requests(requests)
|
||||
connections = []
|
||||
|
||||
@ -237,6 +290,7 @@ module HTTPX
|
||||
connections
|
||||
end
|
||||
|
||||
# returns the array of HTTPX::Response objects corresponding to the array of HTTPX::Request +requests+.
|
||||
def receive_requests(requests, connections)
|
||||
# @type var responses: Array[response]
|
||||
responses = []
|
||||
@ -290,6 +344,11 @@ module HTTPX
|
||||
klass.instance_variable_set(:@callbacks, @callbacks.dup)
|
||||
end
|
||||
|
||||
# returns a new HTTPX::Session instance, with the plugin pointed by +pl+ loaded.
|
||||
#
|
||||
# session_with_retries = session.plugin(:retries)
|
||||
# session_with_custom = session.plugin(CustomPlugin)
|
||||
#
|
||||
def plugin(pl, options = nil, &block)
|
||||
# raise Error, "Cannot add a plugin to a frozen config" if frozen?
|
||||
pl = Plugins.load_plugin(pl) if pl.is_a?(Symbol)
|
||||
|
@ -37,10 +37,9 @@ module HTTPX
|
||||
|
||||
def form: () -> Hash[String, untyped]
|
||||
|
||||
private
|
||||
def initialize: (Request request, String | Integer status, String version, headers?) -> void
|
||||
|
||||
def initialize: (Request request, String | Integer status, String version, headers?) -> untyped
|
||||
def no_data?: () -> bool
|
||||
private
|
||||
|
||||
def decode:(Transcoder::_Decode transcoder, ?untyped options) -> untyped
|
||||
end
|
||||
|
Loading…
x
Reference in New Issue
Block a user