httpx 1.0.2 → 1.1.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/README.md +2 -2
- data/doc/release_notes/1_1_0.md +32 -0
- data/lib/httpx/adapters/faraday.rb +28 -19
- data/lib/httpx/connection/http1.rb +10 -3
- data/lib/httpx/connection/http2.rb +1 -1
- data/lib/httpx/connection.rb +51 -12
- data/lib/httpx/domain_name.rb +6 -2
- data/lib/httpx/errors.rb +32 -0
- data/lib/httpx/io/ssl.rb +3 -1
- data/lib/httpx/io/tcp.rb +4 -2
- data/lib/httpx/io.rb +5 -1
- data/lib/httpx/options.rb +48 -1
- data/lib/httpx/plugins/expect.rb +10 -8
- data/lib/httpx/pool.rb +0 -1
- data/lib/httpx/request/body.rb +22 -9
- data/lib/httpx/request.rb +63 -4
- data/lib/httpx/resolver/native.rb +2 -2
- data/lib/httpx/resolver/system.rb +1 -1
- data/lib/httpx/response/body.rb +30 -5
- data/lib/httpx/response/buffer.rb +20 -14
- data/lib/httpx/response.rb +95 -16
- data/lib/httpx/selector.rb +2 -2
- data/lib/httpx/session.rb +61 -1
- data/lib/httpx/timers.rb +33 -8
- data/lib/httpx/transcoder/json.rb +1 -1
- data/lib/httpx/transcoder/utils/inflater.rb +19 -0
- data/lib/httpx/version.rb +1 -1
- data/sig/connection/http1.rbs +1 -1
- data/sig/connection/http2.rbs +1 -1
- data/sig/connection.rbs +4 -1
- data/sig/io/tcp.rbs +1 -1
- data/sig/options.rbs +2 -2
- data/sig/pool.rbs +1 -1
- data/sig/request/body.rbs +0 -2
- data/sig/request.rbs +9 -3
- data/sig/resolver/native.rbs +1 -1
- data/sig/resolver.rbs +1 -1
- data/sig/response/body.rbs +0 -1
- data/sig/response.rbs +11 -3
- data/sig/timers.rbs +17 -7
- data/sig/transcoder/utils/inflater.rbs +12 -0
- metadata +6 -2
data/lib/httpx/request.rb
CHANGED
@@ -4,20 +4,50 @@ require "delegate"
|
|
4
4
|
require "forwardable"
|
5
5
|
|
6
6
|
module HTTPX
|
7
|
+
# Defines how an HTTP request is handled internally, both in terms of making attributes accessible,
|
8
|
+
# as well as maintaining the state machine which manages streaming the request onto the wire.
|
7
9
|
class Request
|
8
10
|
extend Forwardable
|
9
11
|
include Callbacks
|
10
12
|
using URIExtensions
|
11
13
|
|
14
|
+
# default value used for "user-agent" header, when not overridden.
|
12
15
|
USER_AGENT = "httpx.rb/#{VERSION}"
|
13
16
|
|
14
|
-
|
17
|
+
# the upcased string HTTP verb for this request.
|
18
|
+
attr_reader :verb
|
15
19
|
|
16
|
-
#
|
20
|
+
# the absolute URI object for this request.
|
21
|
+
attr_reader :uri
|
22
|
+
|
23
|
+
# an HTTPX::Headers object containing the request HTTP headers.
|
24
|
+
attr_reader :headers
|
25
|
+
|
26
|
+
# an HTTPX::Request::Body object containing the request body payload (or +nil+, whenn there is none).
|
27
|
+
attr_reader :body
|
28
|
+
|
29
|
+
# a symbol describing which frame is currently being flushed.
|
30
|
+
attr_reader :state
|
31
|
+
|
32
|
+
# an HTTPX::Options object containing request options.
|
33
|
+
attr_reader :options
|
34
|
+
|
35
|
+
# the corresponding HTTPX::Response object, when there is one.
|
36
|
+
attr_reader :response
|
37
|
+
|
38
|
+
# Exception raised during enumerable body writes.
|
17
39
|
attr_reader :drain_error
|
18
40
|
|
41
|
+
# The IP address from the peer server.
|
42
|
+
attr_accessor :peer_address
|
43
|
+
|
44
|
+
attr_writer :persistent
|
45
|
+
|
46
|
+
# will be +true+ when request body has been completely flushed.
|
19
47
|
def_delegator :@body, :empty?
|
20
48
|
|
49
|
+
# initializes the instance with the given +verb+, an absolute or relative +uri+, and the
|
50
|
+
# request options.
|
21
51
|
def initialize(verb, uri, options = {})
|
22
52
|
@verb = verb.to_s.upcase
|
23
53
|
@options = Options.new(options)
|
@@ -37,20 +67,30 @@ module HTTPX
|
|
37
67
|
|
38
68
|
@body = @options.request_body_class.new(@headers, @options)
|
39
69
|
@state = :idle
|
70
|
+
@response = nil
|
71
|
+
@peer_address = nil
|
72
|
+
@persistent = @options.persistent
|
40
73
|
end
|
41
74
|
|
75
|
+
# the read timeout defied for this requet.
|
42
76
|
def read_timeout
|
43
77
|
@options.timeout[:read_timeout]
|
44
78
|
end
|
45
79
|
|
80
|
+
# the write timeout defied for this requet.
|
46
81
|
def write_timeout
|
47
82
|
@options.timeout[:write_timeout]
|
48
83
|
end
|
49
84
|
|
85
|
+
# the request timeout defied for this requet.
|
50
86
|
def request_timeout
|
51
87
|
@options.timeout[:request_timeout]
|
52
88
|
end
|
53
89
|
|
90
|
+
def persistent?
|
91
|
+
@persistent
|
92
|
+
end
|
93
|
+
|
54
94
|
def trailers?
|
55
95
|
defined?(@trailers)
|
56
96
|
end
|
@@ -59,6 +99,7 @@ module HTTPX
|
|
59
99
|
@trailers ||= @options.headers_class.new
|
60
100
|
end
|
61
101
|
|
102
|
+
# returns +:r+ or +:w+, depending on whether the request is waiting for a response or flushing.
|
62
103
|
def interests
|
63
104
|
return :r if @state == :done || @state == :expect
|
64
105
|
|
@@ -69,10 +110,12 @@ module HTTPX
|
|
69
110
|
@headers = @headers.merge(h)
|
70
111
|
end
|
71
112
|
|
113
|
+
# the URI scheme of the request +uri+.
|
72
114
|
def scheme
|
73
115
|
@uri.scheme
|
74
116
|
end
|
75
117
|
|
118
|
+
# sets the +response+ on this request.
|
76
119
|
def response=(response)
|
77
120
|
return unless response
|
78
121
|
|
@@ -85,6 +128,7 @@ module HTTPX
|
|
85
128
|
emit(:response_started, response)
|
86
129
|
end
|
87
130
|
|
131
|
+
# returnns the URI path of the request +uri+.
|
88
132
|
def path
|
89
133
|
path = uri.path.dup
|
90
134
|
path = +"" if path.nil?
|
@@ -93,16 +137,28 @@ module HTTPX
|
|
93
137
|
path
|
94
138
|
end
|
95
139
|
|
96
|
-
#
|
140
|
+
# returs the URI authority of the request.
|
141
|
+
#
|
142
|
+
# session.build_request("GET", "https://google.com/query").authority #=> "google.com"
|
143
|
+
# session.build_request("GET", "http://internal:3182/a").authority #=> "internal:3182"
|
97
144
|
def authority
|
98
145
|
@uri.authority
|
99
146
|
end
|
100
147
|
|
101
|
-
#
|
148
|
+
# returs the URI origin of the request.
|
149
|
+
#
|
150
|
+
# session.build_request("GET", "https://google.com/query").authority #=> "https://google.com"
|
151
|
+
# session.build_request("GET", "http://internal:3182/a").authority #=> "http://internal:3182"
|
102
152
|
def origin
|
103
153
|
@uri.origin
|
104
154
|
end
|
105
155
|
|
156
|
+
# returs the URI query string of the request (when available).
|
157
|
+
#
|
158
|
+
# session.build_request("GET", "https://search.com").query #=> ""
|
159
|
+
# session.build_request("GET", "https://search.com?q=a").query #=> "q=a"
|
160
|
+
# session.build_request("GET", "https://search.com", params: { q: "a"}).query #=> "q=a"
|
161
|
+
# session.build_request("GET", "https://search.com?q=a", params: { foo: "bar"}).query #=> "q=a&foo&bar"
|
106
162
|
def query
|
107
163
|
return @query if defined?(@query)
|
108
164
|
|
@@ -114,6 +170,7 @@ module HTTPX
|
|
114
170
|
@query = query.join("&")
|
115
171
|
end
|
116
172
|
|
173
|
+
# consumes and returns the next available chunk of request body that can be sent
|
117
174
|
def drain_body
|
118
175
|
return nil if @body.nil?
|
119
176
|
|
@@ -139,6 +196,7 @@ module HTTPX
|
|
139
196
|
end
|
140
197
|
# :nocov:
|
141
198
|
|
199
|
+
# moves on to the +nextstate+ of the request state machine (when all preconditions are met)
|
142
200
|
def transition(nextstate)
|
143
201
|
case nextstate
|
144
202
|
when :idle
|
@@ -173,6 +231,7 @@ module HTTPX
|
|
173
231
|
nil
|
174
232
|
end
|
175
233
|
|
234
|
+
# whether the request supports the 100-continue handshake and already processed the 100 response.
|
176
235
|
def expects?
|
177
236
|
@headers["expect"] == "100-continue" && @informational_status == 100 && !@response
|
178
237
|
end
|
@@ -30,7 +30,7 @@ module HTTPX
|
|
30
30
|
nameserver = nameserver[family] if nameserver.is_a?(Hash)
|
31
31
|
Array(nameserver)
|
32
32
|
end
|
33
|
-
@ndots = @resolver_options
|
33
|
+
@ndots = @resolver_options.fetch(:ndots, 1)
|
34
34
|
@search = Array(@resolver_options[:search]).map { |srch| srch.scan(/[^.]+/) }
|
35
35
|
@_timeouts = Array(@resolver_options[:timeouts])
|
36
36
|
@timeouts = Hash.new { |timeouts, host| timeouts[host] = @_timeouts.dup }
|
@@ -103,7 +103,7 @@ module HTTPX
|
|
103
103
|
@timeouts.values_at(*hosts).reject(&:empty?).map(&:first).min
|
104
104
|
end
|
105
105
|
|
106
|
-
def
|
106
|
+
def handle_socket_timeout(interval)
|
107
107
|
do_retry(interval)
|
108
108
|
end
|
109
109
|
|
data/lib/httpx/response/body.rb
CHANGED
@@ -1,19 +1,27 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module HTTPX
|
4
|
+
# Implementation of the HTTP Response body as a buffer which implements the IO writer protocol
|
5
|
+
# (for buffering the response payload), the IO reader protocol (for consuming the response payload),
|
6
|
+
# and can be iterated over (via #each, which yields the payload in chunks).
|
4
7
|
class Response::Body
|
5
|
-
|
8
|
+
# the payload encoding (i.e. "utf-8", "ASCII-8BIT")
|
9
|
+
attr_reader :encoding
|
6
10
|
|
11
|
+
# Array of encodings contained in the response "content-encoding" header.
|
12
|
+
attr_reader :encodings
|
13
|
+
|
14
|
+
# initialized with the corresponding HTTPX::Response +response+ and HTTPX::Options +options+.
|
7
15
|
def initialize(response, options)
|
8
16
|
@response = response
|
9
17
|
@headers = response.headers
|
10
18
|
@options = options
|
11
|
-
@threshold_size = options.body_threshold_size
|
12
19
|
@window_size = options.window_size
|
13
20
|
@encoding = response.content_type.charset || Encoding::BINARY
|
14
21
|
@encodings = []
|
15
22
|
@length = 0
|
16
23
|
@buffer = nil
|
24
|
+
@reader = nil
|
17
25
|
@state = :idle
|
18
26
|
initialize_inflaters
|
19
27
|
end
|
@@ -28,6 +36,8 @@ module HTTPX
|
|
28
36
|
@state == :closed
|
29
37
|
end
|
30
38
|
|
39
|
+
# write the response payload +chunk+ into the buffer. Inflates the chunk when required
|
40
|
+
# and supported.
|
31
41
|
def write(chunk)
|
32
42
|
return if @state == :closed
|
33
43
|
|
@@ -44,6 +54,7 @@ module HTTPX
|
|
44
54
|
size
|
45
55
|
end
|
46
56
|
|
57
|
+
# reads a chunk from the payload (implementation of the IO reader protocol).
|
47
58
|
def read(*args)
|
48
59
|
return unless @buffer
|
49
60
|
|
@@ -55,10 +66,13 @@ module HTTPX
|
|
55
66
|
@reader.read(*args)
|
56
67
|
end
|
57
68
|
|
69
|
+
# size of the decoded response payload. May differ from "content-length" header if
|
70
|
+
# response was encoded over-the-wire.
|
58
71
|
def bytesize
|
59
72
|
@length
|
60
73
|
end
|
61
74
|
|
75
|
+
# yields the payload in chunks.
|
62
76
|
def each
|
63
77
|
return enum_for(__method__) unless block_given?
|
64
78
|
|
@@ -74,12 +88,14 @@ module HTTPX
|
|
74
88
|
end
|
75
89
|
end
|
76
90
|
|
91
|
+
# returns the declared filename in the "contennt-disposition" header, when present.
|
77
92
|
def filename
|
78
93
|
return unless @headers.key?("content-disposition")
|
79
94
|
|
80
95
|
Utils.get_filename(@headers["content-disposition"])
|
81
96
|
end
|
82
97
|
|
98
|
+
# returns the full response payload as a string.
|
83
99
|
def to_s
|
84
100
|
return "".b unless @buffer
|
85
101
|
|
@@ -88,10 +104,16 @@ module HTTPX
|
|
88
104
|
|
89
105
|
alias_method :to_str, :to_s
|
90
106
|
|
107
|
+
# whether the payload is empty.
|
91
108
|
def empty?
|
92
109
|
@length.zero?
|
93
110
|
end
|
94
111
|
|
112
|
+
# copies the payload to +dest+.
|
113
|
+
#
|
114
|
+
# body.copy_to("path/to/file")
|
115
|
+
# body.copy_to(Pathname.new("path/to/file"))
|
116
|
+
# body.copy_to(File.new("path/to/file"))
|
95
117
|
def copy_to(dest)
|
96
118
|
return unless @buffer
|
97
119
|
|
@@ -132,6 +154,7 @@ module HTTPX
|
|
132
154
|
end
|
133
155
|
# :nocov:
|
134
156
|
|
157
|
+
# rewinds the response payload buffer.
|
135
158
|
def rewind
|
136
159
|
return unless @buffer
|
137
160
|
|
@@ -144,6 +167,8 @@ module HTTPX
|
|
144
167
|
private
|
145
168
|
|
146
169
|
def initialize_inflaters
|
170
|
+
@inflaters = nil
|
171
|
+
|
147
172
|
return unless @headers.key?("content-encoding")
|
148
173
|
|
149
174
|
return unless @options.decompress_response_body
|
@@ -168,7 +193,7 @@ module HTTPX
|
|
168
193
|
return unless @state == :idle
|
169
194
|
|
170
195
|
@buffer = Response::Buffer.new(
|
171
|
-
threshold_size: @
|
196
|
+
threshold_size: @options.body_threshold_size,
|
172
197
|
bytesize: @length,
|
173
198
|
encoding: @encoding
|
174
199
|
)
|
@@ -179,7 +204,7 @@ module HTTPX
|
|
179
204
|
@state = nextstate
|
180
205
|
end
|
181
206
|
|
182
|
-
def _with_same_buffer_pos
|
207
|
+
def _with_same_buffer_pos # :nodoc:
|
183
208
|
return yield unless @buffer && @buffer.respond_to?(:pos)
|
184
209
|
|
185
210
|
# @type ivar @buffer: StringIO | Tempfile
|
@@ -193,7 +218,7 @@ module HTTPX
|
|
193
218
|
end
|
194
219
|
|
195
220
|
class << self
|
196
|
-
def initialize_inflater_by_encoding(encoding, response, **kwargs)
|
221
|
+
def initialize_inflater_by_encoding(encoding, response, **kwargs) # :nodoc:
|
197
222
|
case encoding
|
198
223
|
when "gzip"
|
199
224
|
Transcoder::GZIP.decode(response, **kwargs)
|
@@ -5,12 +5,15 @@ require "stringio"
|
|
5
5
|
require "tempfile"
|
6
6
|
|
7
7
|
module HTTPX
|
8
|
+
# wraps and delegates to an internal buffer, which can be a StringIO or a Tempfile.
|
8
9
|
class Response::Buffer < SimpleDelegator
|
10
|
+
# initializes buffer with the +threshold_size+ over which the payload gets buffer to a tempfile,
|
11
|
+
# the initial +bytesize+, and the +encoding+.
|
9
12
|
def initialize(threshold_size:, bytesize: 0, encoding: Encoding::BINARY)
|
10
13
|
@threshold_size = threshold_size
|
11
14
|
@bytesize = bytesize
|
12
15
|
@encoding = encoding
|
13
|
-
|
16
|
+
@buffer = StringIO.new("".b)
|
14
17
|
super(@buffer)
|
15
18
|
end
|
16
19
|
|
@@ -20,16 +23,19 @@ module HTTPX
|
|
20
23
|
@buffer = other.instance_variable_get(:@buffer).dup
|
21
24
|
end
|
22
25
|
|
26
|
+
# size in bytes of the buffered content.
|
23
27
|
def size
|
24
28
|
@bytesize
|
25
29
|
end
|
26
30
|
|
31
|
+
# writes the +chunk+ into the buffer.
|
27
32
|
def write(chunk)
|
28
33
|
@bytesize += chunk.bytesize
|
29
34
|
try_upgrade_buffer
|
30
35
|
@buffer.write(chunk)
|
31
36
|
end
|
32
37
|
|
38
|
+
# returns the buffered content as a string.
|
33
39
|
def to_s
|
34
40
|
case @buffer
|
35
41
|
when StringIO
|
@@ -49,6 +55,7 @@ module HTTPX
|
|
49
55
|
end
|
50
56
|
end
|
51
57
|
|
58
|
+
# closes the buffer.
|
52
59
|
def close
|
53
60
|
@buffer.close
|
54
61
|
@buffer.unlink if @buffer.respond_to?(:unlink)
|
@@ -56,28 +63,27 @@ module HTTPX
|
|
56
63
|
|
57
64
|
private
|
58
65
|
|
66
|
+
# initializes the buffer into a StringIO, or turns it into a Tempfile when the threshold
|
67
|
+
# has been reached.
|
59
68
|
def try_upgrade_buffer
|
60
|
-
|
61
|
-
aux = @buffer
|
62
|
-
|
63
|
-
@buffer = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
|
69
|
+
return unless @bytesize > @threshold_size
|
64
70
|
|
65
|
-
|
66
|
-
aux.rewind
|
67
|
-
::IO.copy_stream(aux, @buffer)
|
68
|
-
aux.close
|
69
|
-
end
|
71
|
+
return if @buffer.is_a?(Tempfile)
|
70
72
|
|
71
|
-
|
72
|
-
return if @buffer
|
73
|
+
aux = @buffer
|
73
74
|
|
74
|
-
|
75
|
+
@buffer = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
|
75
76
|
|
77
|
+
if aux
|
78
|
+
aux.rewind
|
79
|
+
::IO.copy_stream(aux, @buffer)
|
80
|
+
aux.close
|
76
81
|
end
|
82
|
+
|
77
83
|
__setobj__(@buffer)
|
78
84
|
end
|
79
85
|
|
80
|
-
def _with_same_buffer_pos
|
86
|
+
def _with_same_buffer_pos # :nodoc:
|
81
87
|
current_pos = @buffer.pos
|
82
88
|
@buffer.rewind
|
83
89
|
begin
|
data/lib/httpx/response.rb
CHANGED
@@ -7,24 +7,48 @@ require "fileutils"
|
|
7
7
|
require "forwardable"
|
8
8
|
|
9
9
|
module HTTPX
|
10
|
+
# Defines a HTTP response is handled internally, with a few properties exposed as attributes,
|
11
|
+
# implements (indirectly, via the +body+) the IO write protocol to internally buffer payloads,
|
12
|
+
# implements the IO reader protocol in order for users to buffer/stream it, acts as an enumerable
|
13
|
+
# (of payload chunks).
|
10
14
|
class Response
|
11
15
|
extend Forwardable
|
12
16
|
include Callbacks
|
13
17
|
|
14
|
-
|
18
|
+
# the HTTP response status code
|
19
|
+
attr_reader :status
|
15
20
|
|
21
|
+
# an HTTPX::Headers object containing the response HTTP headers.
|
22
|
+
attr_reader :headers
|
23
|
+
|
24
|
+
# a HTTPX::Response::Body object wrapping the response body.
|
25
|
+
attr_reader :body
|
26
|
+
|
27
|
+
# The HTTP protocol version used to fetch the response.
|
28
|
+
attr_reader :version
|
29
|
+
|
30
|
+
# returns the response body buffered in a string.
|
16
31
|
def_delegator :@body, :to_s
|
17
32
|
|
18
33
|
def_delegator :@body, :to_str
|
19
34
|
|
35
|
+
# implements the IO reader +#read+ interface.
|
20
36
|
def_delegator :@body, :read
|
21
37
|
|
38
|
+
# copies the response body to a different location.
|
22
39
|
def_delegator :@body, :copy_to
|
23
40
|
|
41
|
+
# closes the body.
|
24
42
|
def_delegator :@body, :close
|
25
43
|
|
44
|
+
# the corresponding request uri.
|
26
45
|
def_delegator :@request, :uri
|
27
46
|
|
47
|
+
# the IP address of the peer server.
|
48
|
+
def_delegator :@request, :peer_address
|
49
|
+
|
50
|
+
# inits the instance with the corresponding +request+ to this response, an the
|
51
|
+
# response HTTP +status+, +version+ and HTTPX::Headers instance of +headers+.
|
28
52
|
def initialize(request, status, version, headers)
|
29
53
|
@request = request
|
30
54
|
@options = request.options
|
@@ -33,32 +57,49 @@ module HTTPX
|
|
33
57
|
@headers = @options.headers_class.new(headers)
|
34
58
|
@body = @options.response_body_class.new(self, @options)
|
35
59
|
@finished = complete?
|
60
|
+
@content_type = nil
|
36
61
|
end
|
37
62
|
|
63
|
+
# merges headers defined in +h+ into the response headers.
|
38
64
|
def merge_headers(h)
|
39
65
|
@headers = @headers.merge(h)
|
40
66
|
end
|
41
67
|
|
68
|
+
# writes +data+ chunk into the response body.
|
42
69
|
def <<(data)
|
43
70
|
@body.write(data)
|
44
71
|
end
|
45
72
|
|
73
|
+
# returns the response mime type, as per what's declared in the content-type header.
|
74
|
+
#
|
75
|
+
# response.content_type #=> "text/plain"
|
46
76
|
def content_type
|
47
77
|
@content_type ||= ContentType.new(@headers["content-type"])
|
48
78
|
end
|
49
79
|
|
80
|
+
# returns whether the response has been fully fetched.
|
50
81
|
def finished?
|
51
82
|
@finished
|
52
83
|
end
|
53
84
|
|
85
|
+
# marks the response as finished, freezes the headers.
|
54
86
|
def finish!
|
55
87
|
@finished = true
|
56
88
|
@headers.freeze
|
57
89
|
end
|
58
90
|
|
91
|
+
# returns whether the response contains body payload.
|
59
92
|
def bodyless?
|
60
93
|
@request.verb == "HEAD" ||
|
61
|
-
|
94
|
+
@status < 200 || # informational response
|
95
|
+
@status == 204 ||
|
96
|
+
@status == 205 ||
|
97
|
+
@status == 304 || begin
|
98
|
+
content_length = @headers["content-length"]
|
99
|
+
return false if content_length.nil?
|
100
|
+
|
101
|
+
content_length == "0"
|
102
|
+
end
|
62
103
|
end
|
63
104
|
|
64
105
|
def complete?
|
@@ -75,32 +116,53 @@ module HTTPX
|
|
75
116
|
end
|
76
117
|
# :nocov:
|
77
118
|
|
119
|
+
# returns an instance of HTTPX::HTTPError if the response has a 4xx or 5xx
|
120
|
+
# status code, or nothing.
|
121
|
+
#
|
122
|
+
# ok_response.error #=> nil
|
123
|
+
# not_found_response.error #=> HTTPX::HTTPError instance, status 404
|
78
124
|
def error
|
79
125
|
return if @status < 400
|
80
126
|
|
81
127
|
HTTPError.new(self)
|
82
128
|
end
|
83
129
|
|
130
|
+
# it raises the exception returned by +error+, or itself otherwise.
|
131
|
+
#
|
132
|
+
# ok_response.raise_for_status #=> ok_response
|
133
|
+
# not_found_response.raise_for_status #=> raises HTTPX::HTTPError exception
|
84
134
|
def raise_for_status
|
85
135
|
return self unless (err = error)
|
86
136
|
|
87
137
|
raise err
|
88
138
|
end
|
89
139
|
|
140
|
+
# decodes the response payload into a ruby object **if** the payload is valid json.
|
141
|
+
#
|
142
|
+
# response.json #≈> { "foo" => "bar" } for "{\"foo\":\"bar\"}" payload
|
143
|
+
# response.json(symbolize_names: true) #≈> { foo: "bar" } for "{\"foo\":\"bar\"}" payload
|
90
144
|
def json(*args)
|
91
145
|
decode(Transcoder::JSON, *args)
|
92
146
|
end
|
93
147
|
|
148
|
+
# decodes the response payload into a ruby object **if** the payload is valid
|
149
|
+
# "application/x-www-urlencoded" or "multipart/form-data".
|
94
150
|
def form
|
95
151
|
decode(Transcoder::Form)
|
96
152
|
end
|
97
153
|
|
154
|
+
# decodes the response payload into a Nokogiri::XML::Node object **if** the payload is valid
|
155
|
+
# "application/xml" (requires the "nokogiri" gem).
|
98
156
|
def xml
|
99
157
|
decode(Transcoder::Xml)
|
100
158
|
end
|
101
159
|
|
102
160
|
private
|
103
161
|
|
162
|
+
# decodes the response payload using the given +transcoder+, which implements the decoding logic.
|
163
|
+
#
|
164
|
+
# +transcoder+ must implement the internal transcoder API, i.e. respond to <tt>decode(HTTPX::Response response)</tt>,
|
165
|
+
# which returns a decoder which responds to <tt>call(HTTPX::Response response, **kwargs)</tt>
|
104
166
|
def decode(transcoder, *args)
|
105
167
|
# TODO: check if content-type is a valid format, i.e. "application/json" for json parsing
|
106
168
|
|
@@ -112,20 +174,9 @@ module HTTPX
|
|
112
174
|
|
113
175
|
decoder.call(self, *args)
|
114
176
|
end
|
115
|
-
|
116
|
-
def no_data?
|
117
|
-
@status < 200 || # informational response
|
118
|
-
@status == 204 ||
|
119
|
-
@status == 205 ||
|
120
|
-
@status == 304 || begin
|
121
|
-
content_length = @headers["content-length"]
|
122
|
-
return false if content_length.nil?
|
123
|
-
|
124
|
-
content_length == "0"
|
125
|
-
end
|
126
|
-
end
|
127
177
|
end
|
128
178
|
|
179
|
+
# Helper class which decodes the HTTP "content-type" header.
|
129
180
|
class ContentType
|
130
181
|
MIME_TYPE_RE = %r{^([^/]+/[^;]+)(?:$|;)}.freeze
|
131
182
|
CHARSET_RE = /;\s*charset=([^;]+)/i.freeze
|
@@ -134,6 +185,9 @@ module HTTPX
|
|
134
185
|
@header_value = header_value
|
135
186
|
end
|
136
187
|
|
188
|
+
# returns the mime type declared in the header.
|
189
|
+
#
|
190
|
+
# ContentType.new("application/json; charset=utf-8").mime_type #=> "application/json"
|
137
191
|
def mime_type
|
138
192
|
return @mime_type if defined?(@mime_type)
|
139
193
|
|
@@ -141,6 +195,10 @@ module HTTPX
|
|
141
195
|
m && @mime_type = m.strip.downcase
|
142
196
|
end
|
143
197
|
|
198
|
+
# returns the charset declared in the header.
|
199
|
+
#
|
200
|
+
# ContentType.new("application/json; charset=utf-8").charset #=> "utf-8"
|
201
|
+
# ContentType.new("text/plain").charset #=> nil
|
144
202
|
def charset
|
145
203
|
return @charset if defined?(@charset)
|
146
204
|
|
@@ -149,14 +207,31 @@ module HTTPX
|
|
149
207
|
end
|
150
208
|
end
|
151
209
|
|
210
|
+
# Wraps an error which has happened while processing an HTTP Request. It has partial
|
211
|
+
# public API parity with HTTPX::Response, so users should rely on it to infer whether
|
212
|
+
# the returned response is one or the other.
|
213
|
+
#
|
214
|
+
# response = HTTPX.get("https://some-domain/path") #=> response is HTTPX::Response or HTTPX::ErrorResponse
|
215
|
+
# response.raise_for_status #=> raises if it wraps an error
|
152
216
|
class ErrorResponse
|
153
217
|
include Loggable
|
154
218
|
extend Forwardable
|
155
219
|
|
156
|
-
|
220
|
+
# the corresponding HTTPX::Request instance.
|
221
|
+
attr_reader :request
|
157
222
|
|
223
|
+
# the HTTPX::Response instance, when there is one (i.e. error happens fetching the response).
|
224
|
+
attr_reader :response
|
225
|
+
|
226
|
+
# the wrapped exception.
|
227
|
+
attr_reader :error
|
228
|
+
|
229
|
+
# the request uri
|
158
230
|
def_delegator :@request, :uri
|
159
231
|
|
232
|
+
# the IP address of the peer server.
|
233
|
+
def_delegator :@request, :peer_address
|
234
|
+
|
160
235
|
def initialize(request, error, options)
|
161
236
|
@request = request
|
162
237
|
@response = request.response if request.response.is_a?(Response)
|
@@ -165,18 +240,22 @@ module HTTPX
|
|
165
240
|
log_exception(@error)
|
166
241
|
end
|
167
242
|
|
243
|
+
# returns the exception full message.
|
168
244
|
def to_s
|
169
245
|
@error.full_message(highlight: false)
|
170
246
|
end
|
171
247
|
|
248
|
+
# closes the error resources.
|
172
249
|
def close
|
173
|
-
@response.close if @response.respond_to?(:close)
|
250
|
+
@response.close if @response && @response.respond_to?(:close)
|
174
251
|
end
|
175
252
|
|
253
|
+
# always true for error responses.
|
176
254
|
def finished?
|
177
255
|
true
|
178
256
|
end
|
179
257
|
|
258
|
+
# raises the wrapped exception.
|
180
259
|
def raise_for_status
|
181
260
|
raise @error
|
182
261
|
end
|
data/lib/httpx/selector.rb
CHANGED
@@ -73,7 +73,7 @@ class HTTPX::Selector
|
|
73
73
|
readers, writers = IO.select(r, w, nil, interval)
|
74
74
|
|
75
75
|
if readers.nil? && writers.nil? && interval
|
76
|
-
[*r, *w].each { |io| io.
|
76
|
+
[*r, *w].each { |io| io.handle_socket_timeout(interval) }
|
77
77
|
return
|
78
78
|
end
|
79
79
|
rescue IOError, SystemCallError
|
@@ -110,7 +110,7 @@ class HTTPX::Selector
|
|
110
110
|
end
|
111
111
|
|
112
112
|
unless result || interval.nil?
|
113
|
-
io.
|
113
|
+
io.handle_socket_timeout(interval)
|
114
114
|
return
|
115
115
|
end
|
116
116
|
# raise HTTPX::TimeoutError.new(interval, "timed out while waiting on select")
|