patron 0.14.0 → 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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -0
  3. data/README.md +83 -49
  4. data/ext/patron/extconf.rb +54 -23
  5. data/ext/patron/session_ext.c +1044 -923
  6. data/lib/patron/compat.rb +57 -0
  7. data/lib/patron/error.rb +21 -13
  8. data/lib/patron/header_parser.rb +27 -25
  9. data/lib/patron/request.rb +98 -144
  10. data/lib/patron/response.rb +28 -24
  11. data/lib/patron/response_decoding.rb +60 -67
  12. data/lib/patron/session.rb +114 -108
  13. data/lib/patron/util.rb +14 -12
  14. data/lib/patron/version.rb +3 -1
  15. data/lib/patron.rb +9 -15
  16. metadata +12 -58
  17. data/.autotest +0 -15
  18. data/.github/dependabot.yml +0 -6
  19. data/.github/workflows/ci.yml +0 -79
  20. data/.gitignore +0 -10
  21. data/.rspec +0 -0
  22. data/.travis.yml +0 -13
  23. data/.yardopts +0 -1
  24. data/Gemfile +0 -18
  25. data/Rakefile +0 -31
  26. data/ext/patron/.gitignore +0 -4
  27. data/ext/patron/membuffer.c +0 -85
  28. data/ext/patron/membuffer.h +0 -78
  29. data/ext/patron/sglib.h +0 -1952
  30. data/lib/patron/proxy_type.rb +0 -11
  31. data/patron.gemspec +0 -39
  32. data/pic.png +0 -0
  33. data/script/console +0 -11
  34. data/script/generate_client_certs +0 -65
  35. data/script/test_server +0 -7
  36. data/spec/header_parser_spec.rb +0 -101
  37. data/spec/patron_spec.rb +0 -28
  38. data/spec/request_spec.rb +0 -89
  39. data/spec/response_spec.rb +0 -145
  40. data/spec/sample_response_headers/headers_wetransfer.txt +0 -18
  41. data/spec/sample_response_headers/headers_wetransfer_with_proxy_status.txt +0 -20
  42. data/spec/sample_response_headers/headers_wetransfer_with_redirect.txt +0 -24
  43. data/spec/sample_response_headers/headers_with_set_cookie.txt +0 -9
  44. data/spec/sample_response_headers/sample_http2_header.txt +0 -16
  45. data/spec/sample_response_headers/webmock_headers_without_trailing_crlf.txt +0 -15
  46. data/spec/session_spec.rb +0 -865
  47. data/spec/session_ssl_spec.rb +0 -431
  48. data/spec/spec_helper.rb +0 -40
  49. data/spec/support/certs/cacert.pem +0 -36
  50. data/spec/support/certs/client.p12 +0 -0
  51. data/spec/support/certs/client_ca.pem +0 -20
  52. data/spec/support/certs/client_cert.pem +0 -20
  53. data/spec/support/certs/client_key.pem +0 -30
  54. data/spec/support/certs/privkey.pem +0 -51
  55. data/spec/support/certs/rogue_client.p12 +0 -0
  56. data/spec/support/config.ru +0 -153
  57. data/spec/support/fork.rb +0 -70
  58. data/spec/support/raw_request_capture.rb +0 -93
  59. data/spec/support/test_server.rb +0 -29
  60. data/spec/support/valgrind.supp +0 -2
  61. 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,40 +1,48 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Patron
2
4
 
3
5
  # Base class for Patron exceptions.
4
- class Error < StandardError; end
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 < Error; end
13
+ class UnsupportedProtocol < Error; end
9
14
 
10
15
  # Gets raised when a request is attempted with an unsupported SSL version.
11
- class UnsupportedSSLVersion < Error; end
16
+ class UnsupportedSSLVersion < Error; end
12
17
 
13
18
  # Gets raised when a request is attempted with an unsupported HTTP version.
14
- class UnsupportedHTTPVersion < Error; end
19
+ class UnsupportedHTTPVersion < Error; end
15
20
 
16
21
  # Gets raised when the URL was not properly formatted.
17
- class URLFormatError < Error; end
22
+ class URLFormatError < Error; end
18
23
 
19
24
  # Gets raised when the remote host name could not be resolved.
20
- class HostResolutionError < Error; end
25
+ class HostResolutionError < Error; end
21
26
 
22
27
  # Gets raised when failing to connect to the remote host.
23
- class ConnectionFailed < Error; end
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 < Error; end
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 < Error; end
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 < Error; end
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 if the progress callback, or an interrupt, aborts the Curl perform() call
37
- class Aborted < Error; end
44
+ # Gets raised when the response body grew past Session#download_byte_limit.
45
+ class DownloadLimitExceeded < Aborted; end
38
46
 
39
47
  # Gets raised when the client certificate or its private key could not be used,
40
48
  # for example when the key password is wrong or the certificate file is missing.
@@ -45,7 +53,7 @@ module Patron
45
53
  # when the server specifies an encoding in `Content-Type`) which the HTML generator then overrides
46
54
  # with a `meta` element.
47
55
  class HeaderCharsetInvalid < Error; end
48
-
56
+
49
57
  # Gets raised when you try to use `decoded_body` but it can't
50
58
  # be represented by your Ruby process's current internal encoding
51
59
  class NonRepresentableBody < HeaderCharsetInvalid; end
@@ -1,30 +1,32 @@
1
- module Patron::HeaderParser
2
- HTTP_STATUS_LINE_START_RE = /^HTTP\/\d(\.\d)? \d+/
3
- HEADER_LINE_START_RE = /^[^:]+\:/
1
+ # frozen_string_literal: true
4
2
 
5
- # Returned for each response parsed out
6
- class SingleResponseHeaders < Struct.new(:status_line, :headers)
7
- end
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
- # Parses a string with lines delimited with CRLF into
10
- # an Array of SingleResponseHeaders objects. libCURL supplies
11
- # us multiple responses in sequence, so if we encounter multiple redirect
12
- # or operate through a proxy - that adds ConnectionEstablished status at
13
- # the beginning of the response - we need to account for parsing
14
- # multiple response headres and potentially preserving them.
15
- #
16
- # @param [String] the string of headers, with responses delimited by empty lines. All lines must end with CRLF
17
- # @return Array<SingleResponseHeaders>
18
- def self.parse(string_of_headers_from_multiple_responses_in_sequence)
19
- responses = []
20
- string_of_headers_from_multiple_responses_in_sequence.each_line do |matched_line|
21
- if matched_line =~ HTTP_STATUS_LINE_START_RE
22
- responses << SingleResponseHeaders.new(matched_line.strip, [])
23
- elsif matched_line =~ HEADER_LINE_START_RE
24
- raise "Header should follow an HTTP status line" unless responses.any?
25
- responses[-1].headers << matched_line.strip
26
- end # else it is the end of the headers for the request
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
@@ -1,199 +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
- # This is basically a data object with validation. Not all fields will be
7
- # used in every request.
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
- # Contains the valid HTTP verbs that can be used to perform requests
11
- VALID_ACTIONS = %w[GET PUT POST DELETE HEAD COPY PATCH]
12
-
13
- # Initializes a new Request, which defaults to the GET HTTP verb and
14
- # has it's timeouts set to 0
15
- def initialize
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
- end
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, :ssl_cert, :ssl_cert_type, :ssl_key, :ssl_key_password
29
- ]
42
+ @auth_type = Request::AuthBasic
43
+ @force_ipv4 = false
30
44
 
31
- WRITER_VARS = [
32
- :url, :username, :password, :file_name, :proxy, :proxy_type, :insecure, :dns_cache_timeout,
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, :ssl_cert, :ssl_cert_type, :ssl_key, :ssl_key_password
35
- ]
45
+ unknown = attributes.keys - ATTRIBUTES
46
+ raise ArgumentError, "Unknown Request attributes: #{unknown.join(', ')}" unless unknown.empty?
36
47
 
37
- attr_reader(*READER_VARS)
38
- attr_writer(*WRITER_VARS)
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]type The type of authentication to use for this request, can be one of
43
- # :basic, :digest, or :any
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
- when :basic, "basic"
52
- Request::AuthBasic
53
- when :digest, "digest"
54
- Request::AuthDigest
55
- when :any, "any"
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 upload data (request body) for the request. If the
63
- # given argument is a Hash, the contents of the hash will be handled
64
- # as form fields and will be form-encoded. The somposition of the request
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] a Hash of form fields to values, or an object that responds to `to_s`
69
+ # @param data [Hash, #to_s, #to_path, nil]
72
70
  def upload_data=(data)
73
- @upload_data = case data
74
- when Hash
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 for the request
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] the name of the HTTP verb
81
+ # @param action [Symbol, String]
84
82
  def action=(action)
85
- if !VALID_ACTIONS.include?(action.to_s.upcase)
86
- raise ArgumentError, "Action must be one of #{VALID_ACTIONS.join(', ')}"
87
- end
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
- # Sets the read timeout for the CURL request, in seconds.
92
- # Can be set to less than a second using a floating-point value.
93
- # Setting the value to 0 will disable the timeout.
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
- # Sets the connect timeout for the CURL request, in seconds.
104
- # Can be set to less than a second using a floating-point value.
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
- @max_redirects = new_max_redirects.to_i
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
- # Sets the headers for the request. Headers muse be set with the right capitalization.
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
- if !new_headers.kind_of?(Hash)
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
- # Sets the receive buffer size. This is a recommendedation value, as CURL is not guaranteed to
139
- # honor this value internally (see https://curl.haxx.se/libcurl/c/CURLOPT_BUFFERSIZE.html).
140
- # By default, CURL uses the maximum possible buffer size, which will be the best especially
141
- # for smaller and quickly-executing requests.
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
- # Returns the set HTTP authentication string for basic authentication.
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
- # Returns the set HTTP verb
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
- # Tells whether this Request is configured the same as the other request
168
- # @return [TrueClass, FalseClass]
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
- READER_VARS.inject(true) do |memo, name|
173
- memo && (self.send(name) == request.send(name))
174
- end
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
- alias_method :==, :eql?
134
+ def hash = to_h.hash
178
135
 
179
- # Returns a Marshalable representation of the Request
180
- # @return [Array]
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
- @ssl_cert, @ssl_cert_type, @ssl_key, @ssl_key_password ]
186
- end
136
+ # @return [Hash] every attribute, so that Marshal round-trips are complete
137
+ def marshal_dump = to_h
187
138
 
188
- # Reinstates instance variables from a marshaled representation
189
- # @param data[Array]
190
- # @return [void]
191
- def marshal_load(data)
192
- @url, @username, @password, @file_name, @proxy, @proxy_type, @insecure,
193
- @ignore_content_length, @multipart, @action, @timeout, @connect_timeout,
194
- @max_redirects, @headers, @auth_type, @upload_data, @buffer_size, @cacert,
195
- @ssl_cert, @ssl_cert_type, @ssl_key, @ssl_key_password = data
139
+ # @param attributes [Hash]
140
+ def marshal_load(attributes)
141
+ initialize(**attributes.compact.except(:progress_callback))
142
+ @progress_callback = attributes[:progress_callback]
196
143
  end
197
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
198
152
  end
199
153
  end
@@ -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
@@ -35,35 +37,37 @@ module Patron
35
37
 
36
38
  # Overridden so that the output is shorter and there is no response body printed
37
39
  def inspect
38
- # Avoid spamming the console with the header and body data
39
40
  "#<Patron::Response @status_line='#{@status_line}'>"
40
41
  end
41
42
 
42
- def initialize(url, status, redirect_count, raw_header_data, body, default_charset = nil)
43
- @url = url.force_encoding(Encoding::ASCII) # the URL is always an ASCII subset, _always_.
44
- @status = status
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
45
53
  @redirect_count = redirect_count
46
- @body = body.force_encoding(Encoding::BINARY) if body
54
+ @body = body && (body.encoding == Encoding::BINARY ? body : body.b)
55
+ @timings = timings
47
56
 
48
- header_data = decode_header_data(raw_header_data)
49
- parse_headers(header_data)
50
- @charset = charset_from_content_type
57
+ parse_headers(decode_header_data(header_data))
58
+ @charset = charset_from_content_type || default_charset
51
59
  end
52
60
 
53
61
  # Tells whether the HTTP response code is less than 400
54
62
  #
55
63
  # @return [Boolean]
56
- def ok?
57
- !error?
58
- end
59
-
64
+ def ok? = !error?
65
+
60
66
  # Tells whether the HTTP response code is larger than 399
61
67
  #
62
68
  # @return [Boolean]
63
- def error?
64
- status >= 400
65
- end
66
-
69
+ def error? = status >= 400
70
+
67
71
  # Tells whether the response body can be decoded losslessly into the curren internal encoding
68
72
  #
69
73
  # @return [Boolean] true if the body is decodable, false if otherwise
@@ -99,9 +103,10 @@ module Patron
99
103
  # @return [String, nil]
100
104
  def decoded_body
101
105
  return unless @body
106
+ return decode_body(true) if frozen? # a shareable Response may not memoize
102
107
  @decoded_body ||= decode_body(true)
103
108
  end
104
-
109
+
105
110
  # Works the same as `decoded_body`, with one substantial difference: characters which can't be represented
106
111
  # in your process' default encoding are going to be replaced with question marks. This can be used for raising
107
112
  # errors when you receive responses which indicate errors on the server you are calling. For example, if you expect
@@ -112,26 +117,25 @@ module Patron
112
117
  # @return [String, nil]
113
118
  def inspectable_body
114
119
  return unless @body
120
+ return decode_body(false) if frozen?
115
121
  @inspectable_body ||= decode_body(false)
116
122
  end
117
123
 
118
124
  private
119
125
 
120
- # Called by the C code to parse and set the headers
121
126
  def parse_headers(header_data_for_multiple_responses)
122
127
  @headers = {}
123
128
 
124
129
  responses = Patron::HeaderParser.parse(header_data_for_multiple_responses)
125
- last_response = responses[-1] # Only use the last response (for proxies and redirects)
130
+ last_response = responses[-1] # proxies and redirects put their own responses in front of the real one
126
131
 
127
132
  @status_line = last_response.status_line
128
133
  last_response.headers.each do |line|
129
134
  hdr, val = line.split(":", 2)
130
-
131
- val.strip! unless val.nil?
135
+ val = val&.strip
132
136
 
133
137
  if @headers.key?(hdr)
134
- @headers[hdr] = [@headers[hdr]] unless @headers[hdr].kind_of? Array
138
+ @headers[hdr] = [@headers[hdr]] unless @headers[hdr].kind_of?(Array)
135
139
  @headers[hdr] << val
136
140
  else
137
141
  @headers[hdr] = val