httpx 1.0.2 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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")
|