patron 0.13.4 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +97 -0
- data/README.md +113 -50
- data/ext/patron/extconf.rb +55 -20
- data/ext/patron/session_ext.c +1092 -761
- data/lib/patron/compat.rb +57 -0
- data/lib/patron/error.rb +25 -13
- data/lib/patron/header_parser.rb +27 -25
- data/lib/patron/request.rb +98 -142
- data/lib/patron/response.rb +33 -24
- data/lib/patron/response_decoding.rb +60 -67
- data/lib/patron/session.rb +127 -104
- data/lib/patron/util.rb +14 -12
- data/lib/patron/version.rb +3 -1
- data/lib/patron.rb +9 -15
- metadata +12 -49
- data/.autotest +0 -15
- data/.github/workflows/ci.yml +0 -26
- data/.gitignore +0 -10
- data/.rspec +0 -0
- data/.travis.yml +0 -13
- data/.yardopts +0 -1
- data/Gemfile +0 -17
- data/Rakefile +0 -31
- data/ext/patron/.gitignore +0 -4
- data/ext/patron/membuffer.c +0 -85
- data/ext/patron/membuffer.h +0 -78
- data/ext/patron/sglib.h +0 -1952
- data/lib/patron/proxy_type.rb +0 -11
- data/patron.gemspec +0 -38
- data/pic.png +0 -0
- data/script/console +0 -11
- data/script/test_server +0 -7
- data/spec/header_parser_spec.rb +0 -101
- data/spec/patron_spec.rb +0 -28
- data/spec/request_spec.rb +0 -77
- data/spec/response_spec.rb +0 -145
- data/spec/sample_response_headers/headers_wetransfer.txt +0 -18
- data/spec/sample_response_headers/headers_wetransfer_with_proxy_status.txt +0 -20
- data/spec/sample_response_headers/headers_wetransfer_with_redirect.txt +0 -24
- data/spec/sample_response_headers/headers_with_set_cookie.txt +0 -9
- data/spec/sample_response_headers/sample_http2_header.txt +0 -16
- data/spec/sample_response_headers/webmock_headers_without_trailing_crlf.txt +0 -15
- data/spec/session_spec.rb +0 -698
- data/spec/session_ssl_spec.rb +0 -313
- data/spec/spec_helper.rb +0 -34
- data/spec/support/certs/cacert.pem +0 -36
- data/spec/support/certs/privkey.pem +0 -51
- data/spec/support/config.ru +0 -146
- data/spec/support/fork.rb +0 -70
- data/spec/support/test_server.rb +0 -26
- data/spec/util_spec.rb +0 -71
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The positional-argument forms of the 0.x API, kept for the 1.0 line so that webmock's
|
|
4
|
+
# adapter and sniffer's wrapper keep working while they move to keywords. Each form warns
|
|
5
|
+
# once. Everything about this lives here on purpose, bar the one line in Session#request: to drop
|
|
6
|
+
# it in 1.1, delete this file, its require in session.rb, that line and spec/compat_spec.rb.
|
|
7
|
+
# See https://github.com/toland/patron/issues/209
|
|
8
|
+
module Patron
|
|
9
|
+
module Compat
|
|
10
|
+
# Kept per Ractor since a non-main Ractor may not touch module state, so each Ractor warns once
|
|
11
|
+
def self.warned = Ractor.current[:patron_compat_warned] ||= {}
|
|
12
|
+
|
|
13
|
+
# uplevel counts frames up to the user's call site: deprecated -> shim method -> caller
|
|
14
|
+
def self.deprecated(form, replacement, uplevel: 2)
|
|
15
|
+
return if warned[form]
|
|
16
|
+
warned[form] = true
|
|
17
|
+
warn "Patron: #{form} is deprecated and will be removed in Patron 1.1, use #{replacement} instead (https://github.com/toland/patron/issues/209)", uplevel: uplevel
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Session.new(hash). The request(action, url, headers, options_hash) form is handled in
|
|
21
|
+
# Session#request itself, see the comment there
|
|
22
|
+
module SessionArguments
|
|
23
|
+
def initialize(*positional, **options, &block)
|
|
24
|
+
options = Compat.hash_to_keywords("Patron::Session.new with a Hash", positional, options)
|
|
25
|
+
super(**options, &block)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Response.new(url, status, redirect_count, header_data, body, default_charset = nil)
|
|
30
|
+
module ResponseArguments
|
|
31
|
+
POSITIONAL = %i[url status redirect_count header_data body default_charset].freeze
|
|
32
|
+
|
|
33
|
+
def new(*positional, **keywords)
|
|
34
|
+
return super(**keywords) if positional.empty?
|
|
35
|
+
|
|
36
|
+
Compat.deprecated("Patron::Response.new with positional arguments", "keyword arguments")
|
|
37
|
+
if positional.size > POSITIONAL.size
|
|
38
|
+
raise ArgumentError, "wrong number of arguments (given #{positional.size}, expected 5..6)"
|
|
39
|
+
end
|
|
40
|
+
super(**POSITIONAL.zip(positional).to_h.compact, **keywords)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.hash_to_keywords(form, positional, keywords)
|
|
45
|
+
return keywords if positional.empty?
|
|
46
|
+
unless positional.size == 1 && positional.first.is_a?(Hash)
|
|
47
|
+
raise ArgumentError, "wrong number of arguments (given #{positional.size}, expected 0)"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
deprecated(form, "keyword arguments", uplevel: 3)
|
|
51
|
+
positional.first.transform_keys(&:to_sym).merge(keywords)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
Session.prepend(Compat::SessionArguments)
|
|
56
|
+
Response.singleton_class.prepend(Compat::ResponseArguments)
|
|
57
|
+
end
|
data/lib/patron/error.rb
CHANGED
|
@@ -1,47 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Patron
|
|
2
4
|
|
|
3
5
|
# Base class for Patron exceptions.
|
|
4
|
-
class Error < StandardError
|
|
6
|
+
class Error < StandardError
|
|
7
|
+
# @return [Integer, nil] the CURLcode libcurl reported, when the error came from a transfer
|
|
8
|
+
attr_reader :curl_code
|
|
9
|
+
end
|
|
5
10
|
|
|
6
11
|
# Gets raised when the URL passed to Patron used a protocol that it does not support.
|
|
7
12
|
# This most likely the result of a misspelled protocol string.
|
|
8
|
-
class UnsupportedProtocol
|
|
13
|
+
class UnsupportedProtocol < Error; end
|
|
9
14
|
|
|
10
15
|
# Gets raised when a request is attempted with an unsupported SSL version.
|
|
11
|
-
class UnsupportedSSLVersion
|
|
16
|
+
class UnsupportedSSLVersion < Error; end
|
|
12
17
|
|
|
13
18
|
# Gets raised when a request is attempted with an unsupported HTTP version.
|
|
14
|
-
class UnsupportedHTTPVersion
|
|
19
|
+
class UnsupportedHTTPVersion < Error; end
|
|
15
20
|
|
|
16
21
|
# Gets raised when the URL was not properly formatted.
|
|
17
|
-
class URLFormatError
|
|
22
|
+
class URLFormatError < Error; end
|
|
18
23
|
|
|
19
24
|
# Gets raised when the remote host name could not be resolved.
|
|
20
|
-
class HostResolutionError
|
|
25
|
+
class HostResolutionError < Error; end
|
|
21
26
|
|
|
22
27
|
# Gets raised when failing to connect to the remote host.
|
|
23
|
-
class ConnectionFailed
|
|
28
|
+
class ConnectionFailed < Error; end
|
|
24
29
|
|
|
25
30
|
# Gets raised when the response was shorter or larger than expected.
|
|
26
31
|
# This happens when the server first reports an expected transfer size,
|
|
27
32
|
# and then delivers data that doesn't match the previously given size.
|
|
28
|
-
class PartialFileError
|
|
33
|
+
class PartialFileError < Error; end
|
|
29
34
|
|
|
30
35
|
# Gets raised on an operation timeout. The specified time-out period was reached.
|
|
31
|
-
class TimeoutError
|
|
36
|
+
class TimeoutError < Error; end
|
|
32
37
|
|
|
33
38
|
# Gets raised on too many redirects. When following redirects, Patron hit the maximum amount.
|
|
34
|
-
class TooManyRedirects
|
|
39
|
+
class TooManyRedirects < Error; end
|
|
40
|
+
|
|
41
|
+
# Gets raised when the request was aborted: by Session#interrupt, or by a progress callback returning false.
|
|
42
|
+
class Aborted < Error; end
|
|
35
43
|
|
|
36
|
-
# Gets raised
|
|
37
|
-
class
|
|
44
|
+
# Gets raised when the response body grew past Session#download_byte_limit.
|
|
45
|
+
class DownloadLimitExceeded < Aborted; end
|
|
46
|
+
|
|
47
|
+
# Gets raised when the client certificate or its private key could not be used,
|
|
48
|
+
# for example when the key password is wrong or the certificate file is missing.
|
|
49
|
+
class ClientCertificateError < Error; end
|
|
38
50
|
|
|
39
51
|
# Gets raised when the server specifies an encoding that could not be found, or has an invalid name,
|
|
40
52
|
# or when the server "lies" about the encoding of the response body (such as can be the case
|
|
41
53
|
# when the server specifies an encoding in `Content-Type`) which the HTML generator then overrides
|
|
42
54
|
# with a `meta` element.
|
|
43
55
|
class HeaderCharsetInvalid < Error; end
|
|
44
|
-
|
|
56
|
+
|
|
45
57
|
# Gets raised when you try to use `decoded_body` but it can't
|
|
46
58
|
# be represented by your Ruby process's current internal encoding
|
|
47
59
|
class NonRepresentableBody < HeaderCharsetInvalid; end
|
data/lib/patron/header_parser.rb
CHANGED
|
@@ -1,30 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
HTTP_STATUS_LINE_START_RE = /^HTTP\/\d(\.\d)? \d+/
|
|
3
|
-
HEADER_LINE_START_RE = /^[^:]+\:/
|
|
1
|
+
# frozen_string_literal: true
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
module Patron
|
|
4
|
+
module HeaderParser
|
|
5
|
+
HTTP_STATUS_LINE_START_RE = /^HTTP\/\d(\.\d)? \d+/
|
|
6
|
+
HEADER_LINE_START_RE = /^[^:]+:/
|
|
7
|
+
|
|
8
|
+
# The status line and header lines of one response
|
|
9
|
+
SingleResponseHeaders = Data.define(:status_line, :headers)
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
end
|
|
11
|
+
# Splits the raw header data libcurl hands us into one SingleResponseHeaders per response.
|
|
12
|
+
# There is more than one when redirects were followed, or when a proxy added its own
|
|
13
|
+
# "Connection established" response in front of the real one.
|
|
14
|
+
#
|
|
15
|
+
# @param raw_headers [String] CRLF-delimited header lines, responses separated by empty lines
|
|
16
|
+
# @return [Array<SingleResponseHeaders>]
|
|
17
|
+
def self.parse(raw_headers)
|
|
18
|
+
responses = []
|
|
19
|
+
current_headers = nil
|
|
20
|
+
raw_headers.each_line do |line|
|
|
21
|
+
if line.match?(HTTP_STATUS_LINE_START_RE)
|
|
22
|
+
current_headers = []
|
|
23
|
+
responses << SingleResponseHeaders.new(status_line: line.strip, headers: current_headers)
|
|
24
|
+
elsif line.match?(HEADER_LINE_START_RE)
|
|
25
|
+
raise Patron::Error, "Header should follow an HTTP status line" unless current_headers
|
|
26
|
+
current_headers << line.strip
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
responses.each { |r| r.headers.freeze }
|
|
27
30
|
end
|
|
28
|
-
responses
|
|
29
31
|
end
|
|
30
32
|
end
|
data/lib/patron/request.rb
CHANGED
|
@@ -1,197 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'patron/util'
|
|
2
4
|
|
|
3
5
|
module Patron
|
|
4
6
|
|
|
5
|
-
# Represents the information necessary for an HTTP request.
|
|
6
|
-
#
|
|
7
|
-
#
|
|
7
|
+
# Represents the information necessary for an HTTP request. A data object with
|
|
8
|
+
# validation in the setters, built by Session#build_request or by hand. A Request
|
|
9
|
+
# can be reused: the extension reads its attributes once when the request starts,
|
|
10
|
+
# so changing the URL and sending it again is fine.
|
|
8
11
|
class Request
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
ATTRIBUTES = %i[
|
|
13
|
+
action url headers timeout connect_timeout dns_cache_timeout max_redirects
|
|
14
|
+
username password auth_type proxy proxy_type insecure cacert
|
|
15
|
+
ssl_version http_version ssl_cert ssl_cert_type ssl_key ssl_key_password
|
|
16
|
+
ignore_content_length buffer_size download_byte_limit progress_callback
|
|
17
|
+
automatic_content_encoding force_ipv4 low_speed_time low_speed_limit
|
|
18
|
+
upload_data file_name multipart
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
# Any HTTP token works as a verb; the ones libcurl does not know natively are sent as custom requests
|
|
22
|
+
ACTION_RE = /\A[A-Za-z][A-Za-z0-9-]*\z/
|
|
23
|
+
|
|
24
|
+
# Attributes without validation of their own
|
|
25
|
+
attr_accessor :url, :username, :password, :file_name, :proxy, :proxy_type, :insecure, :dns_cache_timeout,
|
|
26
|
+
:ignore_content_length, :multipart, :cacert, :ssl_version, :http_version,
|
|
27
|
+
:automatic_content_encoding, :force_ipv4, :download_byte_limit, :low_speed_time,
|
|
28
|
+
:low_speed_limit, :progress_callback, :ssl_cert, :ssl_cert_type, :ssl_key, :ssl_key_password
|
|
29
|
+
|
|
30
|
+
attr_reader :action, :headers, :timeout, :connect_timeout, :max_redirects, :auth_type, :buffer_size, :upload_data
|
|
31
|
+
|
|
32
|
+
# Defaults to a GET with no timeouts and unlimited redirects. Attributes may be given as
|
|
33
|
+
# keywords; `action` and `multipart` are applied first since `upload_data=` depends on them.
|
|
34
|
+
#
|
|
35
|
+
# @param attributes [Hash] attribute values by name
|
|
36
|
+
def initialize(**attributes)
|
|
16
37
|
@action = :get
|
|
17
38
|
@headers = {}
|
|
18
|
-
@timeout = 0
|
|
19
|
-
@connect_timeout = 0
|
|
39
|
+
@timeout = 0.0
|
|
40
|
+
@connect_timeout = 0.0
|
|
20
41
|
@max_redirects = -1
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
READER_VARS = [
|
|
24
|
-
:url, :username, :password, :file_name, :proxy, :proxy_type, :insecure,
|
|
25
|
-
:ignore_content_length, :multipart, :action, :timeout, :connect_timeout, :dns_cache_timeout,
|
|
26
|
-
:max_redirects, :headers, :auth_type, :upload_data, :buffer_size, :cacert,
|
|
27
|
-
:ssl_version, :http_version, :automatic_content_encoding, :force_ipv4, :download_byte_limit,
|
|
28
|
-
:low_speed_time, :low_speed_limit, :progress_callback
|
|
29
|
-
]
|
|
42
|
+
@auth_type = Request::AuthBasic
|
|
43
|
+
@force_ipv4 = false
|
|
30
44
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
:ignore_content_length, :multipart, :cacert, :ssl_version, :http_version, :automatic_content_encoding, :force_ipv4, :download_byte_limit,
|
|
34
|
-
:low_speed_time, :low_speed_limit, :progress_callback
|
|
35
|
-
]
|
|
45
|
+
unknown = attributes.keys - ATTRIBUTES
|
|
46
|
+
raise ArgumentError, "Unknown Request attributes: #{unknown.join(', ')}" unless unknown.empty?
|
|
36
47
|
|
|
37
|
-
|
|
38
|
-
|
|
48
|
+
%i[action multipart].each { |first| public_send("#{first}=", attributes[first]) if attributes.key?(first) }
|
|
49
|
+
attributes.each { |name, value| public_send("#{name}=", value) unless %i[action multipart].include?(name) }
|
|
50
|
+
end
|
|
39
51
|
|
|
40
52
|
# Set the type of authentication to use for this request.
|
|
41
53
|
#
|
|
42
|
-
# @param [String, Symbol]
|
|
43
|
-
|
|
44
|
-
#
|
|
45
|
-
# @example
|
|
46
|
-
# sess.username = "foo"
|
|
47
|
-
# sess.password = "sekrit"
|
|
48
|
-
# sess.auth_type = :digest
|
|
49
|
-
def auth_type=(type=:basic)
|
|
54
|
+
# @param type [String, Symbol, Integer] `:basic`, `:digest`, `:any`, or a `Request::Auth*` constant
|
|
55
|
+
def auth_type=(type)
|
|
50
56
|
@auth_type = case type
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
Request::AuthAny
|
|
57
|
-
else
|
|
58
|
-
raise "#{type.inspect} is an unknown authentication type"
|
|
57
|
+
in :basic | "basic" | nil then Request::AuthBasic
|
|
58
|
+
in :digest | "digest" then Request::AuthDigest
|
|
59
|
+
in :any | "any" then Request::AuthAny
|
|
60
|
+
in Integer then type
|
|
61
|
+
else raise ArgumentError, "#{type.inspect} is an unknown authentication type"
|
|
59
62
|
end
|
|
60
63
|
end
|
|
61
64
|
|
|
62
|
-
# Sets the
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
# body is then going to be handled by Curl.
|
|
66
|
-
#
|
|
67
|
-
# If the given `data` is any other object, it is going to be treated as a stringable
|
|
68
|
-
# request body (JSON or other verbatim type) and will have it's `to_s` method called
|
|
69
|
-
# before sending out the request.
|
|
65
|
+
# Sets the request body. A Hash is form-encoded, unless the request is multipart, in which
|
|
66
|
+
# case libcurl composes the body from it. Anything else is sent as its `to_s`, or uploaded
|
|
67
|
+
# from the file it names when it responds to `to_path`.
|
|
70
68
|
#
|
|
71
|
-
# @param data[Hash, #to_s
|
|
69
|
+
# @param data [Hash, #to_s, #to_path, nil]
|
|
72
70
|
def upload_data=(data)
|
|
73
|
-
@upload_data =
|
|
74
|
-
|
|
75
|
-
self.multipart ? data : Util.build_query_string_from_hash(data, action == :post)
|
|
71
|
+
@upload_data = if data.is_a?(Hash) && !multipart
|
|
72
|
+
Util.build_query_string_from_hash(data, action == :post)
|
|
76
73
|
else
|
|
77
74
|
data
|
|
78
75
|
end
|
|
79
76
|
end
|
|
80
77
|
|
|
81
|
-
# Sets the HTTP verb
|
|
78
|
+
# Sets the HTTP verb. Any HTTP token is accepted (#180); the ones libcurl does not know
|
|
79
|
+
# natively are sent as custom requests.
|
|
82
80
|
#
|
|
83
|
-
# @param action[String]
|
|
81
|
+
# @param action [Symbol, String]
|
|
84
82
|
def action=(action)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
@action = action.downcase.to_sym
|
|
83
|
+
name = action.to_s
|
|
84
|
+
raise ArgumentError, "#{action.inspect} is not a valid HTTP verb" unless name.match?(ACTION_RE)
|
|
85
|
+
@action = name.downcase.to_sym
|
|
89
86
|
end
|
|
90
87
|
|
|
91
|
-
#
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
#
|
|
95
|
-
# @param new_timeout[Integer] the number of seconds to wait before raising a timeout error
|
|
96
|
-
def timeout=(new_timeout)
|
|
97
|
-
if new_timeout && new_timeout.to_f < 0
|
|
98
|
-
raise ArgumentError, "Timeout must be a positive number"
|
|
99
|
-
end
|
|
100
|
-
@timeout = new_timeout.to_f
|
|
88
|
+
# @param seconds [Numeric, nil] transfer timeout, fractional seconds allowed, 0 or nil to disable
|
|
89
|
+
def timeout=(seconds)
|
|
90
|
+
@timeout = validate_timeout(seconds)
|
|
101
91
|
end
|
|
102
92
|
|
|
103
|
-
#
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
# @param new_timeout[Integer] the number of seconds to wait before raising a timeout error
|
|
107
|
-
def connect_timeout=(new_timeout)
|
|
108
|
-
if new_timeout && new_timeout.to_f < 0
|
|
109
|
-
raise ArgumentError, "Timeout must be a positive number"
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
@connect_timeout = new_timeout.to_f
|
|
93
|
+
# @param seconds [Numeric, nil] connect timeout, fractional seconds allowed, 0 or nil to disable
|
|
94
|
+
def connect_timeout=(seconds)
|
|
95
|
+
@connect_timeout = validate_timeout(seconds)
|
|
113
96
|
end
|
|
114
|
-
|
|
115
|
-
# Sets the maximum number of redirects that are going to be followed.
|
|
116
|
-
#
|
|
117
|
-
# @param new_max_redirects[Integer] The number of redirects to follow, or `-1` for unlimited redirects.
|
|
118
|
-
def max_redirects=(new_max_redirects)
|
|
119
|
-
if new_max_redirects.to_i < -1
|
|
120
|
-
raise ArgumentError, "Max redirects must be a positive integer, 0 or -1"
|
|
121
|
-
end
|
|
122
97
|
|
|
123
|
-
|
|
98
|
+
# @param count [Integer] -1 for unlimited, 0 to not follow redirects
|
|
99
|
+
def max_redirects=(count)
|
|
100
|
+
raise ArgumentError, "Max redirects must be a positive integer, 0 or -1" if count.to_i < -1
|
|
101
|
+
@max_redirects = count.to_i
|
|
124
102
|
end
|
|
125
103
|
|
|
126
|
-
#
|
|
127
|
-
# The previously set headers will be replaced.
|
|
128
|
-
#
|
|
129
|
-
# @param new_headers[Hash] the hash of headers to set.
|
|
104
|
+
# @param new_headers [Hash] replaces the headers set so far
|
|
130
105
|
def headers=(new_headers)
|
|
131
|
-
|
|
132
|
-
raise ArgumentError, "Headers must be a hash"
|
|
133
|
-
end
|
|
134
|
-
|
|
106
|
+
raise ArgumentError, "Headers must be a hash" unless new_headers.is_a?(Hash)
|
|
135
107
|
@headers = new_headers
|
|
136
108
|
end
|
|
137
109
|
|
|
138
|
-
#
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
#
|
|
143
|
-
# @param buffer_size[Integer,nil] the desired buffer size, or `nil` for automatic buffer size
|
|
144
|
-
def buffer_size=(buffer_size)
|
|
145
|
-
if buffer_size != nil && buffer_size.to_i < 1
|
|
146
|
-
raise ArgumentError, "Buffer size must be a positive integer greater than 0 or nil"
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
@buffer_size = buffer_size != nil ? buffer_size.to_i : nil
|
|
110
|
+
# @param size [Integer, nil] the receive buffer size libcurl should use, nil for its default
|
|
111
|
+
def buffer_size=(size)
|
|
112
|
+
raise ArgumentError, "Buffer size must be a positive integer greater than 0 or nil" if !size.nil? && size.to_i < 1
|
|
113
|
+
@buffer_size = size&.to_i
|
|
150
114
|
end
|
|
151
115
|
|
|
152
|
-
#
|
|
153
|
-
#
|
|
154
|
-
# @return [String, NilClass] the authentication string or nil if no authentication is used
|
|
116
|
+
# @return [String, nil] the "user:password" pair for HTTP authentication, or nil when either is missing
|
|
155
117
|
def credentials
|
|
156
118
|
return nil if username.nil? || password.nil?
|
|
157
119
|
"#{username}:#{password}"
|
|
158
120
|
end
|
|
159
121
|
|
|
160
|
-
#
|
|
161
|
-
|
|
162
|
-
# @return [String] the HTTP verb
|
|
163
|
-
def action_name
|
|
164
|
-
@action.to_s.upcase
|
|
165
|
-
end
|
|
122
|
+
# @return [String] the HTTP verb in upper case
|
|
123
|
+
def action_name = action.to_s.upcase
|
|
166
124
|
|
|
167
|
-
#
|
|
168
|
-
|
|
169
|
-
def eql?(request)
|
|
170
|
-
return false unless Request === request
|
|
125
|
+
# @return [Hash{Symbol => Object}] every attribute by name
|
|
126
|
+
def to_h = ATTRIBUTES.to_h { |name| [name, public_send(name)] }
|
|
171
127
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
128
|
+
# @return [Boolean] whether the other request is configured identically
|
|
129
|
+
def eql?(other)
|
|
130
|
+
other.is_a?(Request) && to_h == other.to_h
|
|
175
131
|
end
|
|
132
|
+
alias == eql?
|
|
176
133
|
|
|
177
|
-
|
|
134
|
+
def hash = to_h.hash
|
|
178
135
|
|
|
179
|
-
#
|
|
180
|
-
|
|
181
|
-
def marshal_dump
|
|
182
|
-
[ @url, @username, @password, @file_name, @proxy, @proxy_type, @insecure,
|
|
183
|
-
@ignore_content_length, @multipart, @action, @timeout, @connect_timeout,
|
|
184
|
-
@max_redirects, @headers, @auth_type, @upload_data, @buffer_size, @cacert ]
|
|
185
|
-
end
|
|
136
|
+
# @return [Hash] every attribute, so that Marshal round-trips are complete
|
|
137
|
+
def marshal_dump = to_h
|
|
186
138
|
|
|
187
|
-
#
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
@url, @username, @password, @file_name, @proxy, @proxy_type, @insecure,
|
|
192
|
-
@ignore_content_length, @multipart, @action, @timeout, @connect_timeout,
|
|
193
|
-
@max_redirects, @headers, @auth_type, @upload_data, @buffer_size, @cacert = data
|
|
139
|
+
# @param attributes [Hash]
|
|
140
|
+
def marshal_load(attributes)
|
|
141
|
+
initialize(**attributes.compact.except(:progress_callback))
|
|
142
|
+
@progress_callback = attributes[:progress_callback]
|
|
194
143
|
end
|
|
195
144
|
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
def validate_timeout(seconds)
|
|
148
|
+
return nil if seconds.nil?
|
|
149
|
+
raise ArgumentError, "Timeout must be a positive number" if seconds.to_f < 0
|
|
150
|
+
seconds.to_f
|
|
151
|
+
end
|
|
196
152
|
end
|
|
197
153
|
end
|
data/lib/patron/response.rb
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Patron
|
|
2
4
|
|
|
3
5
|
# Represents the response from the HTTP server.
|
|
4
6
|
class Response
|
|
5
7
|
include ResponseDecoding
|
|
6
|
-
|
|
8
|
+
|
|
7
9
|
# @return [String] the original URL used to perform the request (contains the final URL after redirects)
|
|
8
10
|
attr_reader :url
|
|
9
11
|
|
|
@@ -19,7 +21,7 @@ module Patron
|
|
|
19
21
|
# @return [String, nil] the response body as a String encoded as `Encoding::BINARY` or
|
|
20
22
|
# or `nil` if the response was written directly to a file
|
|
21
23
|
attr_reader :body
|
|
22
|
-
|
|
24
|
+
|
|
23
25
|
# @return [Hash] the response headers. If there were multiple headers received for the same value
|
|
24
26
|
# (like "Cookie"), the header values will be within an Array under the key for the header, in order.
|
|
25
27
|
attr_reader :headers
|
|
@@ -28,37 +30,44 @@ module Patron
|
|
|
28
30
|
# to be a valid charset name, just stored. To check the charset for validity, use #body_decodable?
|
|
29
31
|
attr_reader :charset
|
|
30
32
|
|
|
33
|
+
# @return [Hash{Symbol => Float}, nil] curl's cumulative transfer marks in seconds since the request
|
|
34
|
+
# started: `:namelookup`, `:connect`, `:appconnect`, `:pretransfer`, `:starttransfer`, `:redirect`
|
|
35
|
+
# and `:total`. `nil` when the response was not built from a curl transfer (e.g. a stub).
|
|
36
|
+
attr_reader :timings
|
|
37
|
+
|
|
31
38
|
# Overridden so that the output is shorter and there is no response body printed
|
|
32
39
|
def inspect
|
|
33
|
-
# Avoid spamming the console with the header and body data
|
|
34
40
|
"#<Patron::Response @status_line='#{@status_line}'>"
|
|
35
41
|
end
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
# @param url [String] the effective URL after redirects
|
|
44
|
+
# @param status [Integer] the HTTP status code
|
|
45
|
+
# @param redirect_count [Integer] how many redirects were followed
|
|
46
|
+
# @param header_data [String] the raw header lines of every response in the chain, CRLF delimited
|
|
47
|
+
# @param body [String, nil] the response body, or nil when it was downloaded to a file
|
|
48
|
+
# @param default_charset [String, nil] the charset to assume when the Content-Type header names none
|
|
49
|
+
# @param timings [Hash, nil] see #timings
|
|
50
|
+
def initialize(url:, status:, header_data:, redirect_count: 0, body: nil, default_charset: nil, timings: nil)
|
|
51
|
+
@url = url
|
|
52
|
+
@status = status
|
|
40
53
|
@redirect_count = redirect_count
|
|
41
|
-
@body
|
|
54
|
+
@body = body && (body.encoding == Encoding::BINARY ? body : body.b)
|
|
55
|
+
@timings = timings
|
|
42
56
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
@charset = charset_from_content_type
|
|
57
|
+
parse_headers(decode_header_data(header_data))
|
|
58
|
+
@charset = charset_from_content_type || default_charset
|
|
46
59
|
end
|
|
47
60
|
|
|
48
61
|
# Tells whether the HTTP response code is less than 400
|
|
49
62
|
#
|
|
50
63
|
# @return [Boolean]
|
|
51
|
-
def ok?
|
|
52
|
-
|
|
53
|
-
end
|
|
54
|
-
|
|
64
|
+
def ok? = !error?
|
|
65
|
+
|
|
55
66
|
# Tells whether the HTTP response code is larger than 399
|
|
56
67
|
#
|
|
57
68
|
# @return [Boolean]
|
|
58
|
-
def error?
|
|
59
|
-
|
|
60
|
-
end
|
|
61
|
-
|
|
69
|
+
def error? = status >= 400
|
|
70
|
+
|
|
62
71
|
# Tells whether the response body can be decoded losslessly into the curren internal encoding
|
|
63
72
|
#
|
|
64
73
|
# @return [Boolean] true if the body is decodable, false if otherwise
|
|
@@ -94,9 +103,10 @@ module Patron
|
|
|
94
103
|
# @return [String, nil]
|
|
95
104
|
def decoded_body
|
|
96
105
|
return unless @body
|
|
106
|
+
return decode_body(true) if frozen? # a shareable Response may not memoize
|
|
97
107
|
@decoded_body ||= decode_body(true)
|
|
98
108
|
end
|
|
99
|
-
|
|
109
|
+
|
|
100
110
|
# Works the same as `decoded_body`, with one substantial difference: characters which can't be represented
|
|
101
111
|
# in your process' default encoding are going to be replaced with question marks. This can be used for raising
|
|
102
112
|
# errors when you receive responses which indicate errors on the server you are calling. For example, if you expect
|
|
@@ -107,26 +117,25 @@ module Patron
|
|
|
107
117
|
# @return [String, nil]
|
|
108
118
|
def inspectable_body
|
|
109
119
|
return unless @body
|
|
120
|
+
return decode_body(false) if frozen?
|
|
110
121
|
@inspectable_body ||= decode_body(false)
|
|
111
122
|
end
|
|
112
123
|
|
|
113
124
|
private
|
|
114
125
|
|
|
115
|
-
# Called by the C code to parse and set the headers
|
|
116
126
|
def parse_headers(header_data_for_multiple_responses)
|
|
117
127
|
@headers = {}
|
|
118
128
|
|
|
119
129
|
responses = Patron::HeaderParser.parse(header_data_for_multiple_responses)
|
|
120
|
-
last_response = responses[-1] #
|
|
130
|
+
last_response = responses[-1] # proxies and redirects put their own responses in front of the real one
|
|
121
131
|
|
|
122
132
|
@status_line = last_response.status_line
|
|
123
133
|
last_response.headers.each do |line|
|
|
124
134
|
hdr, val = line.split(":", 2)
|
|
125
|
-
|
|
126
|
-
val.strip! unless val.nil?
|
|
135
|
+
val = val&.strip
|
|
127
136
|
|
|
128
137
|
if @headers.key?(hdr)
|
|
129
|
-
@headers[hdr] = [@headers[hdr]] unless @headers[hdr].kind_of?
|
|
138
|
+
@headers[hdr] = [@headers[hdr]] unless @headers[hdr].kind_of?(Array)
|
|
130
139
|
@headers[hdr] << val
|
|
131
140
|
else
|
|
132
141
|
@headers[hdr] = val
|