httpx 1.7.7 → 1.8.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/doc/release_notes/1_7_8.md +5 -0
- data/doc/release_notes/1_8_0.md +100 -0
- data/lib/httpx/adapters/datadog.rb +3 -1
- data/lib/httpx/connection/http1.rb +10 -1
- data/lib/httpx/connection/http2.rb +37 -4
- data/lib/httpx/connection.rb +76 -7
- data/lib/httpx/errors.rb +8 -1
- data/lib/httpx/io/tcp.rb +11 -1
- data/lib/httpx/options.rb +16 -4
- data/lib/httpx/parser/http1.rb +8 -2
- data/lib/httpx/plugins/auth.rb +52 -4
- data/lib/httpx/plugins/{response_cache → cache}/file_store.rb +1 -1
- data/lib/httpx/plugins/{response_cache → cache}/store.rb +1 -1
- data/lib/httpx/plugins/cache.rb +221 -0
- data/lib/httpx/plugins/fiber_concurrency.rb +50 -3
- data/lib/httpx/plugins/ntlm_v2_auth.rb +92 -0
- data/lib/httpx/plugins/oauth.rb +66 -14
- data/lib/httpx/plugins/proxy.rb +5 -0
- data/lib/httpx/plugins/response_cache.rb +26 -105
- data/lib/httpx/plugins/retries.rb +13 -5
- data/lib/httpx/plugins/server_sent_events.rb +158 -0
- data/lib/httpx/plugins/ssrf_filter.rb +16 -1
- data/lib/httpx/plugins/stream.rb +7 -3
- data/lib/httpx/plugins/tracing.rb +15 -4
- data/lib/httpx/request.rb +18 -1
- data/lib/httpx/resolver/cache/file.rb +56 -0
- data/lib/httpx/resolver/native.rb +14 -3
- data/lib/httpx/response/body.rb +4 -2
- data/lib/httpx/response.rb +9 -1
- data/lib/httpx/selector.rb +7 -1
- data/lib/httpx/version.rb +1 -1
- data/sig/chainable.rbs +3 -0
- data/sig/connection/http1.rbs +1 -1
- data/sig/connection/http2.rbs +1 -1
- data/sig/connection.rbs +11 -8
- data/sig/errors.rbs +9 -3
- data/sig/httpx.rbs +2 -0
- data/sig/io/tcp.rbs +2 -0
- data/sig/loggable.rbs +4 -0
- data/sig/options.rbs +25 -12
- data/sig/parser/http1.rbs +3 -1
- data/sig/plugins/auth/ntlm.rbs +1 -1
- data/sig/plugins/{response_cache → cache}/file_store.rbs +2 -2
- data/sig/plugins/{response_cache → cache}/store.rbs +2 -2
- data/sig/plugins/cache.rbs +69 -0
- data/sig/plugins/fiber_concurrency.rbs +4 -0
- data/sig/plugins/ntlm_v2_auth.rbs +36 -0
- data/sig/plugins/response_cache.rbs +13 -38
- data/sig/plugins/retries.rbs +5 -5
- data/sig/plugins/server_sent_events.rbs +45 -0
- data/sig/plugins/ssrf_filter.rbs +5 -1
- data/sig/plugins/stream.rbs +1 -1
- data/sig/plugins/stream_bidi.rbs +0 -2
- data/sig/plugins/webdav.rbs +1 -1
- data/sig/pool.rbs +2 -2
- data/sig/request.rbs +7 -3
- data/sig/resolver/cache/file.rbs +13 -0
- data/sig/resolver/entry.rbs +1 -1
- data/sig/resolver/https.rbs +3 -3
- data/sig/resolver/multi.rbs +1 -1
- data/sig/resolver/native.rbs +5 -5
- data/sig/resolver/resolver.rbs +1 -3
- data/sig/resolver/system.rbs +2 -2
- data/sig/resolver.rbs +3 -0
- data/sig/response.rbs +3 -0
- data/sig/selector.rbs +11 -8
- data/sig/timers.rbs +5 -5
- data/sig/transcoder/body.rbs +1 -1
- data/sig/transcoder/gzip.rbs +3 -2
- data/sig/transcoder/multipart.rbs +4 -1
- data/sig/transcoder/utils/deflater.rbs +2 -0
- data/sig/transcoder.rbs +2 -0
- data/sig/utils.rbs +1 -1
- metadata +19 -7
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
module HTTPX
|
|
4
4
|
module Plugins
|
|
5
5
|
#
|
|
6
|
-
# This plugin
|
|
6
|
+
# This plugin caches and reuses responses based on HTTP caching directives defined by
|
|
7
|
+
# the [HTTP Caching RFC](https://www.rfc-editor.org/rfc/rfc9111.html)
|
|
7
8
|
#
|
|
8
9
|
# https://gitlab.com/os85/httpx/wikis/Response-Cache
|
|
9
10
|
#
|
|
@@ -15,9 +16,8 @@ module HTTPX
|
|
|
15
16
|
private_constant :CACHEABLE_STATUS_CODES
|
|
16
17
|
|
|
17
18
|
class << self
|
|
18
|
-
def load_dependencies(
|
|
19
|
-
|
|
20
|
-
require_relative "response_cache/file_store"
|
|
19
|
+
def load_dependencies(klass)
|
|
20
|
+
klass.plugin(:cache)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
# whether the +response+ can be stored in the response cache.
|
|
@@ -47,7 +47,6 @@ module HTTPX
|
|
|
47
47
|
def extra_options(options)
|
|
48
48
|
options.merge(
|
|
49
49
|
supported_vary_headers: SUPPORTED_VARY_HEADERS,
|
|
50
|
-
response_cache_store: :store,
|
|
51
50
|
)
|
|
52
51
|
end
|
|
53
52
|
end
|
|
@@ -56,58 +55,18 @@ module HTTPX
|
|
|
56
55
|
#
|
|
57
56
|
# :supported_vary_headers :: array of header values that will be considered for a "vary" header based cache validation
|
|
58
57
|
# (defaults to {SUPPORTED_VARY_HEADERS}).
|
|
59
|
-
# :response_cache_store :: object where cached responses are fetch from or stored in; defaults to <tt>:store</tt> (in-memory
|
|
60
|
-
# cache), can be set to <tt>:file_store</tt> (file system cache store) as well, or any object which
|
|
61
|
-
# abides by the Cache Store Interface
|
|
62
|
-
#
|
|
63
|
-
# The Cache Store Interface requires implementation of the following methods:
|
|
64
|
-
#
|
|
65
|
-
# * +#get(request) -> response or nil+
|
|
66
|
-
# * +#set(request, response) -> void+
|
|
67
|
-
# * +#clear() -> void+)
|
|
68
58
|
#
|
|
69
59
|
module OptionsMethods
|
|
70
60
|
private
|
|
71
61
|
|
|
72
|
-
def option_response_cache_store(value)
|
|
73
|
-
case value
|
|
74
|
-
when :store
|
|
75
|
-
Store.new
|
|
76
|
-
when :file_store
|
|
77
|
-
FileStore.new
|
|
78
|
-
else
|
|
79
|
-
value
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
62
|
def option_supported_vary_headers(value)
|
|
84
63
|
Array(value).sort
|
|
85
64
|
end
|
|
86
65
|
end
|
|
87
66
|
|
|
88
67
|
module InstanceMethods
|
|
89
|
-
# wipes out all cached responses from the cache store.
|
|
90
|
-
def clear_response_cache
|
|
91
|
-
@options.response_cache_store.clear
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def build_request(*)
|
|
95
|
-
request = super
|
|
96
|
-
return request unless cacheable_request?(request)
|
|
97
|
-
|
|
98
|
-
prepare_cache(request)
|
|
99
|
-
|
|
100
|
-
request
|
|
101
|
-
end
|
|
102
|
-
|
|
103
68
|
private
|
|
104
69
|
|
|
105
|
-
def send_request(request, *)
|
|
106
|
-
return request if request.response
|
|
107
|
-
|
|
108
|
-
super
|
|
109
|
-
end
|
|
110
|
-
|
|
111
70
|
def fetch_response(request, *)
|
|
112
71
|
response = super
|
|
113
72
|
|
|
@@ -117,11 +76,7 @@ module HTTPX
|
|
|
117
76
|
log { "returning cached response for #{request.uri}" }
|
|
118
77
|
|
|
119
78
|
response.copy_from_cached!
|
|
120
|
-
|
|
121
|
-
unless response.cached?
|
|
122
|
-
log { "caching response for #{request.uri}..." }
|
|
123
|
-
request.options.response_cache_store.set(request, response)
|
|
124
|
-
end
|
|
79
|
+
|
|
125
80
|
end
|
|
126
81
|
|
|
127
82
|
response
|
|
@@ -130,21 +85,13 @@ module HTTPX
|
|
|
130
85
|
# will either assign a still-fresh cached response to +request+, or set up its HTTP
|
|
131
86
|
# cache invalidation headers in case it's not fresh anymore.
|
|
132
87
|
def prepare_cache(request)
|
|
133
|
-
|
|
88
|
+
super
|
|
134
89
|
|
|
135
|
-
return
|
|
90
|
+
return if request.response # already cached
|
|
136
91
|
|
|
137
|
-
cached_response
|
|
92
|
+
cached_response = retrieve_cached_response(request)
|
|
138
93
|
|
|
139
|
-
|
|
140
|
-
cached_response = cached_response.dup
|
|
141
|
-
cached_response.mark_as_cached!
|
|
142
|
-
request.response = cached_response
|
|
143
|
-
request.emit_response(cached_response)
|
|
144
|
-
return
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
request.cached_response = cached_response
|
|
94
|
+
return unless cached_response && match_by_vary?(request, cached_response)
|
|
148
95
|
|
|
149
96
|
if !request.headers.key?("if-modified-since") && (last_modified = cached_response.headers["last-modified"])
|
|
150
97
|
request.headers.add("if-modified-since", last_modified)
|
|
@@ -156,20 +103,31 @@ module HTTPX
|
|
|
156
103
|
end
|
|
157
104
|
|
|
158
105
|
def cacheable_request?(request)
|
|
159
|
-
|
|
106
|
+
(
|
|
107
|
+
request.cacheable_verb? &&
|
|
160
108
|
(
|
|
161
109
|
!request.headers.key?("cache-control") || !request.headers.get("cache-control").include?("no-store")
|
|
162
110
|
)
|
|
111
|
+
) || super
|
|
163
112
|
end
|
|
164
113
|
|
|
165
|
-
|
|
114
|
+
def cacheable_response?(_, response)
|
|
115
|
+
ResponseCache.cacheable_response?(response) || super
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# +cached_response+ is still valid if it's still fresh
|
|
119
|
+
def valid_cached_response?(_, cached_response)
|
|
120
|
+
cached_response.fresh?
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# whether the +cached_response+ complies with the directives set by the +request+ "vary" header
|
|
166
124
|
# (true when none is available).
|
|
167
|
-
def match_by_vary?(request,
|
|
168
|
-
vary =
|
|
125
|
+
def match_by_vary?(request, cached_response)
|
|
126
|
+
vary = cached_response.vary
|
|
169
127
|
|
|
170
128
|
return true unless vary
|
|
171
129
|
|
|
172
|
-
original_request =
|
|
130
|
+
original_request = cached_response.original_request
|
|
173
131
|
|
|
174
132
|
if vary == %w[*]
|
|
175
133
|
request.options.supported_vary_headers.each do |field|
|
|
@@ -186,19 +144,6 @@ module HTTPX
|
|
|
186
144
|
end
|
|
187
145
|
|
|
188
146
|
module RequestMethods
|
|
189
|
-
# points to a previously cached Response corresponding to this request.
|
|
190
|
-
attr_accessor :cached_response
|
|
191
|
-
|
|
192
|
-
def initialize(*)
|
|
193
|
-
super
|
|
194
|
-
@cached_response = nil
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
def merge_headers(*)
|
|
198
|
-
super
|
|
199
|
-
@response_cache_key = nil
|
|
200
|
-
end
|
|
201
|
-
|
|
202
147
|
# returns whether this request is cacheable as per HTTP caching rules.
|
|
203
148
|
def cacheable_verb?
|
|
204
149
|
CACHEABLE_VERBS.include?(@verb)
|
|
@@ -220,29 +165,13 @@ module HTTPX
|
|
|
220
165
|
end
|
|
221
166
|
|
|
222
167
|
module ResponseMethods
|
|
223
|
-
attr_writer :
|
|
168
|
+
attr_writer :revalidated_at
|
|
224
169
|
|
|
225
170
|
def initialize(*)
|
|
226
171
|
super
|
|
227
|
-
@cached = false
|
|
228
172
|
@revalidated_at = nil
|
|
229
173
|
end
|
|
230
174
|
|
|
231
|
-
# a copy of the request this response was originally cached from
|
|
232
|
-
def original_request
|
|
233
|
-
@original_request || @request
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
# whether this Response was duplicated from a previously {RequestMethods#cached_response}.
|
|
237
|
-
def cached?
|
|
238
|
-
@cached
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
# sets this Response as being duplicated from a previously cached response.
|
|
242
|
-
def mark_as_cached!
|
|
243
|
-
@cached = true
|
|
244
|
-
end
|
|
245
|
-
|
|
246
175
|
# eager-copies the response headers and body from {RequestMethods#cached_response}.
|
|
247
176
|
def copy_from_cached!
|
|
248
177
|
cached_response = @request.cached_response
|
|
@@ -333,14 +262,6 @@ module HTTPX
|
|
|
333
262
|
Time.now
|
|
334
263
|
end
|
|
335
264
|
end
|
|
336
|
-
|
|
337
|
-
module ResponseBodyMethods
|
|
338
|
-
def decode_chunk(chunk)
|
|
339
|
-
return chunk if @response.cached?
|
|
340
|
-
|
|
341
|
-
super
|
|
342
|
-
end
|
|
343
|
-
end
|
|
344
265
|
end
|
|
345
266
|
register_plugin :response_cache, ResponseCache
|
|
346
267
|
end
|
|
@@ -53,16 +53,18 @@ module HTTPX
|
|
|
53
53
|
end
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
# returns the time to wait before resending +request+ as per the polynomial backoff retry strategy
|
|
56
|
+
# returns the time to wait before resending +request+ as per the polynomial backoff retry strategy,
|
|
57
|
+
# where base is 1 and exponent is 2.
|
|
57
58
|
def retry_after_polynomial_backoff(request, _)
|
|
58
59
|
offset = request.options.max_retries - request.retries
|
|
59
|
-
|
|
60
|
+
1 * ((offset - 1)**2)
|
|
60
61
|
end
|
|
61
62
|
|
|
62
|
-
# returns the time to wait before resending +request+ as per the exponential backoff retry strategy
|
|
63
|
+
# returns the time to wait before resending +request+ as per the exponential backoff retry strategy,
|
|
64
|
+
# where base is 2
|
|
63
65
|
def retry_after_exponential_backoff(request, _)
|
|
64
66
|
offset = request.options.max_retries - request.retries
|
|
65
|
-
(offset - 1)
|
|
67
|
+
2**(offset - 1)
|
|
66
68
|
end
|
|
67
69
|
end
|
|
68
70
|
|
|
@@ -186,7 +188,7 @@ module HTTPX
|
|
|
186
188
|
|
|
187
189
|
# returns whether the +ex+ exception happend for a retriable request.
|
|
188
190
|
def retryable_error?(ex, _)
|
|
189
|
-
RETRYABLE_ERRORS.any? { |klass| ex.is_a?(klass) }
|
|
191
|
+
RETRYABLE_ERRORS.any? { |klass| ex.is_a?(klass) } && !ex.is_a?(TotalRequestTimeoutError)
|
|
190
192
|
end
|
|
191
193
|
|
|
192
194
|
def proxy_error?(request, response, _)
|
|
@@ -200,7 +202,13 @@ module HTTPX
|
|
|
200
202
|
|
|
201
203
|
def when_to_retry(request, response, options)
|
|
202
204
|
retry_after = options.retry_after
|
|
205
|
+
|
|
206
|
+
return unless retry_after
|
|
207
|
+
|
|
203
208
|
retry_after = retry_after.call(request, response) if retry_after.respond_to?(:call)
|
|
209
|
+
|
|
210
|
+
return unless retry_after
|
|
211
|
+
|
|
204
212
|
# apply jitter
|
|
205
213
|
if (jitter = request.options.retry_jitter)
|
|
206
214
|
retry_after = jitter.call(retry_after)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTPX
|
|
4
|
+
module Plugins
|
|
5
|
+
#
|
|
6
|
+
# This plugin implements convenience methods for Server Sent Events streams.
|
|
7
|
+
#
|
|
8
|
+
# https://gitlab.com/os85/httpx/wikis/Server-Sent-Events
|
|
9
|
+
#
|
|
10
|
+
module ServerSentEvents
|
|
11
|
+
Message = if RUBY_VERSION >= "3.2.0" # rubocop:disable Naming/ConstantName
|
|
12
|
+
Data.define(:data, :event, :id, :retry_after) do
|
|
13
|
+
def initialize(event: nil, id: nil, retry_after: nil, **kwargs)
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
else
|
|
18
|
+
Struct.new(:data, :event, :id, :retry_after, keyword_init: true)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
def subplugins
|
|
23
|
+
{
|
|
24
|
+
retries: ServerSentEventsRetries,
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def load_dependencies(klass)
|
|
29
|
+
klass.plugin(:stream)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# adds support for the following options:
|
|
34
|
+
#
|
|
35
|
+
# :event_stream :: whether the request is a server-sent events text event stream (defaults to <tt>false</tt>).
|
|
36
|
+
module OptionsMethods
|
|
37
|
+
def option_event_stream(val)
|
|
38
|
+
val
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
module InstanceMethods
|
|
43
|
+
def request(*args, **options)
|
|
44
|
+
options[:stream] = true if options[:event_stream]
|
|
45
|
+
|
|
46
|
+
super
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def build_request(*)
|
|
50
|
+
super.tap do |request|
|
|
51
|
+
if request.options.event_stream
|
|
52
|
+
request.headers["accept"] = "text/event-stream"
|
|
53
|
+
request.headers["cache-control"] = "no-cache"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
module RequestMethods
|
|
60
|
+
attr_accessor :last_server_sent_message
|
|
61
|
+
|
|
62
|
+
def initialize(*)
|
|
63
|
+
super
|
|
64
|
+
|
|
65
|
+
@last_server_sent_message = nil
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
module StreamResponseMethods
|
|
70
|
+
# yields each event Message as the server emits them.
|
|
71
|
+
def each_message(&block)
|
|
72
|
+
return enum_for(__method__) unless block
|
|
73
|
+
|
|
74
|
+
payload = {}
|
|
75
|
+
|
|
76
|
+
each_line do |line|
|
|
77
|
+
if line.empty?
|
|
78
|
+
if payload[:comment]
|
|
79
|
+
payload.clear
|
|
80
|
+
next
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
next if payload.empty?
|
|
84
|
+
|
|
85
|
+
message = Message.new(**payload)
|
|
86
|
+
|
|
87
|
+
payload.clear
|
|
88
|
+
|
|
89
|
+
@request.last_server_sent_message = message
|
|
90
|
+
|
|
91
|
+
yield message
|
|
92
|
+
else
|
|
93
|
+
type, value = line.split(": ", 2)
|
|
94
|
+
|
|
95
|
+
case type
|
|
96
|
+
when "data"
|
|
97
|
+
type = type.to_sym
|
|
98
|
+
if payload.key?(type)
|
|
99
|
+
payload[type] << "\n" << value
|
|
100
|
+
else
|
|
101
|
+
payload[type] = value
|
|
102
|
+
end
|
|
103
|
+
when "id", "event", "retry"
|
|
104
|
+
type = type.to_sym
|
|
105
|
+
raise_format_error(line) if payload.key?(type) || value.empty?
|
|
106
|
+
|
|
107
|
+
type = :retry_after if type == :retry # avoid using keyword
|
|
108
|
+
|
|
109
|
+
payload[type] = value
|
|
110
|
+
else
|
|
111
|
+
# skip if it's a comment
|
|
112
|
+
if line.start_with?(":")
|
|
113
|
+
payload[:comment] = true
|
|
114
|
+
next
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
raise_format_error(line)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def raise_format_error(line)
|
|
126
|
+
raise Error, "'#{line}': invalid or unsupported event stream format"
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
module ServerSentEventsRetries
|
|
131
|
+
module InstanceMethods
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
def prepare_to_retry(request, *)
|
|
135
|
+
super
|
|
136
|
+
|
|
137
|
+
last_message = request.last_server_sent_message
|
|
138
|
+
|
|
139
|
+
return unless last_message && last_message.id
|
|
140
|
+
|
|
141
|
+
request.headers["last-event-id"] = last_message.id
|
|
142
|
+
ensure
|
|
143
|
+
request.last_server_sent_message = nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def when_to_retry(request, *)
|
|
147
|
+
retry_after = request.last_server_sent_message&.retry_after
|
|
148
|
+
|
|
149
|
+
retry_after / 1_000.0 if retry_after # original in milliseconds
|
|
150
|
+
|
|
151
|
+
request.last_server_sent_message&.retry_after && super
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
register_plugin(:server_sent_events, ServerSentEvents)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -90,12 +90,22 @@ module HTTPX
|
|
|
90
90
|
# adds support for the following options:
|
|
91
91
|
#
|
|
92
92
|
# :allowed_schemes :: list of URI schemes allowed (defaults to <tt>["https", "http"]</tt>)
|
|
93
|
+
# :extra_unsafe_ranges :: A list of IP ranges (or addresses) that will be filtered, in addition to the defaults
|
|
94
|
+
# :safe_private_ranges :: A list of IP ranges (or addresses) that will not be filtered, even if they'd be filtered by default
|
|
93
95
|
module OptionsMethods
|
|
94
96
|
private
|
|
95
97
|
|
|
96
98
|
def option_allowed_schemes(value)
|
|
97
99
|
Array(value)
|
|
98
100
|
end
|
|
101
|
+
|
|
102
|
+
def option_extra_unsafe_ranges(value)
|
|
103
|
+
Array(value).map { |v| v.is_a?(IPAddr) ? v : IPAddr.new(v) }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def option_safe_private_ranges(value)
|
|
107
|
+
Array(value).map { |v| v.is_a?(IPAddr) ? v : IPAddr.new(v) }
|
|
108
|
+
end
|
|
99
109
|
end
|
|
100
110
|
|
|
101
111
|
module InstanceMethods
|
|
@@ -132,7 +142,12 @@ module HTTPX
|
|
|
132
142
|
end
|
|
133
143
|
|
|
134
144
|
def addresses=(addrs)
|
|
135
|
-
addrs.reject!
|
|
145
|
+
addrs.reject! do |ipaddr|
|
|
146
|
+
ipaddr = ipaddr.address
|
|
147
|
+
next false if @options.safe_private_ranges&.any? { |r| r.include?(ipaddr) }
|
|
148
|
+
|
|
149
|
+
SsrfFilter.unsafe_ip_address?(ipaddr) || @options.extra_unsafe_ranges&.any? { |r| r.include?(ipaddr) }
|
|
150
|
+
end
|
|
136
151
|
|
|
137
152
|
raise ServerSideRequestForgeryError, "#{@origin.host} has no public IP addresses" if addrs.empty?
|
|
138
153
|
|
data/lib/httpx/plugins/stream.rb
CHANGED
|
@@ -46,8 +46,8 @@ module HTTPX
|
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
def each_line
|
|
50
|
-
return enum_for(__method__) unless
|
|
49
|
+
def each_line(&block)
|
|
50
|
+
return enum_for(__method__) unless block
|
|
51
51
|
|
|
52
52
|
line = "".b
|
|
53
53
|
|
|
@@ -55,7 +55,11 @@ module HTTPX
|
|
|
55
55
|
line << chunk
|
|
56
56
|
|
|
57
57
|
while (idx = line.index("\n"))
|
|
58
|
-
|
|
58
|
+
if idx.zero?
|
|
59
|
+
yield ""
|
|
60
|
+
else
|
|
61
|
+
yield line.byteslice(0..(idx - 1))
|
|
62
|
+
end
|
|
59
63
|
|
|
60
64
|
line = line.byteslice((idx + 1)..-1)
|
|
61
65
|
end
|
|
@@ -111,10 +111,11 @@ module HTTPX::Plugins
|
|
|
111
111
|
@init_time = ::Time.now.utc
|
|
112
112
|
end
|
|
113
113
|
|
|
114
|
-
def
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
114
|
+
def send_request_to_parser(request)
|
|
115
|
+
if connecting?
|
|
116
|
+
# request span timeframe should include the time it took to connect.
|
|
117
|
+
request.init_time ||= @init_time
|
|
118
|
+
end
|
|
118
119
|
|
|
119
120
|
super
|
|
120
121
|
end
|
|
@@ -126,6 +127,16 @@ module HTTPX::Plugins
|
|
|
126
127
|
# the connection is back to :idle, and ready to connect again.
|
|
127
128
|
@init_time = ::Time.now.utc
|
|
128
129
|
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def ping(request)
|
|
134
|
+
# if a connection is probed for liveness, the request timeframe should include
|
|
135
|
+
# it too.
|
|
136
|
+
request.init_time ||= ::Time.now.utc
|
|
137
|
+
|
|
138
|
+
super
|
|
139
|
+
end
|
|
129
140
|
end
|
|
130
141
|
end
|
|
131
142
|
register_plugin :tracing, Tracing
|
data/lib/httpx/request.rb
CHANGED
|
@@ -39,6 +39,10 @@ module HTTPX
|
|
|
39
39
|
# Exception raised during enumerable body writes.
|
|
40
40
|
attr_reader :drain_error
|
|
41
41
|
|
|
42
|
+
# when this request is sent via HTTP/2, it'll use this hash of options to set the priority of the
|
|
43
|
+
# respective HTTP/2 frame.
|
|
44
|
+
attr_reader :http2_stream_options
|
|
45
|
+
|
|
42
46
|
# The IP address from the peer server.
|
|
43
47
|
attr_accessor :peer_address
|
|
44
48
|
|
|
@@ -70,6 +74,7 @@ module HTTPX
|
|
|
70
74
|
# :form :: hash of array of key-values which will be form-urlencoded- or multipart-encoded in requests body payload.
|
|
71
75
|
# :json :: hash of array of key-values which will be JSON-encoded in requests body payload.
|
|
72
76
|
# :xml :: Nokogiri XML nodes which will be encoded in requests body payload.
|
|
77
|
+
# :http2_stream_options :: hash of options to be used to set the HTTP/2 priority by sending an initial PRIORITY frame.
|
|
73
78
|
#
|
|
74
79
|
# :body, :form, :json and :xml are all mutually exclusive, i.e. only one of them gets picked up.
|
|
75
80
|
def initialize(verb, uri, options, params = EMPTY_HASH)
|
|
@@ -81,6 +86,8 @@ module HTTPX
|
|
|
81
86
|
|
|
82
87
|
@query_params = params.delete(:params) if params.key?(:params)
|
|
83
88
|
|
|
89
|
+
@http2_stream_options = params.key?(:http2_stream_options) ? params.delete(:http2_stream_options) : EMPTY_HASH
|
|
90
|
+
|
|
84
91
|
@body = options.request_body_class.new(@headers, options, **params)
|
|
85
92
|
|
|
86
93
|
@options = @body.options
|
|
@@ -100,7 +107,7 @@ module HTTPX
|
|
|
100
107
|
@connection = @response =
|
|
101
108
|
@drainer = @peer_address =
|
|
102
109
|
@informational_status = @on_response_arrived = nil
|
|
103
|
-
@ping = false
|
|
110
|
+
@ping = @started = false
|
|
104
111
|
@persistent = @options.persistent
|
|
105
112
|
@active_timeouts = []
|
|
106
113
|
end
|
|
@@ -142,6 +149,11 @@ module HTTPX
|
|
|
142
149
|
@options.timeout[:request_timeout]
|
|
143
150
|
end
|
|
144
151
|
|
|
152
|
+
# the total request timeout defined for this request.
|
|
153
|
+
def total_request_timeout
|
|
154
|
+
@options.timeout[:total_request_timeout]
|
|
155
|
+
end
|
|
156
|
+
|
|
145
157
|
def persistent?
|
|
146
158
|
@persistent
|
|
147
159
|
end
|
|
@@ -167,6 +179,10 @@ module HTTPX
|
|
|
167
179
|
@state != :done
|
|
168
180
|
end
|
|
169
181
|
|
|
182
|
+
def started?
|
|
183
|
+
@started
|
|
184
|
+
end
|
|
185
|
+
|
|
170
186
|
# merges +h+ into the instance of HTTPX::Headers of the request.
|
|
171
187
|
def merge_headers(h)
|
|
172
188
|
@headers = @headers.merge(h)
|
|
@@ -287,6 +303,7 @@ module HTTPX
|
|
|
287
303
|
when :headers
|
|
288
304
|
return unless @state == :idle
|
|
289
305
|
|
|
306
|
+
@started = true
|
|
290
307
|
when :body
|
|
291
308
|
return unless @state == :headers ||
|
|
292
309
|
@state == :expect
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pstore"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
|
|
6
|
+
module HTTPX
|
|
7
|
+
module Resolver::Cache
|
|
8
|
+
# Implementation of a file resolver cache.
|
|
9
|
+
class File < Base
|
|
10
|
+
# default path where the resolver cache is stored. It's versioned, as the file may
|
|
11
|
+
# change format in-between releases, and it'd signal it as corrupted.
|
|
12
|
+
DEFAULT_PATH = ::File.join(Dir.tmpdir, "httpx-ruby-#{VERSION}.cache")
|
|
13
|
+
|
|
14
|
+
def initialize(path = DEFAULT_PATH)
|
|
15
|
+
super()
|
|
16
|
+
@store = PStore.new(path, true)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def get(hostname)
|
|
20
|
+
now = Utils.now
|
|
21
|
+
@store.transaction do
|
|
22
|
+
lookups = @store[:lookups] || EMPTY_HASH
|
|
23
|
+
hostnames = @store[:hostnames] || EMPTY
|
|
24
|
+
|
|
25
|
+
_get(hostname, lookups, hostnames, now)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def set(hostname, family, entries)
|
|
30
|
+
@store.transaction do
|
|
31
|
+
lookups = @store[:lookups] || {}
|
|
32
|
+
hostnames = @store[:hostnames] || []
|
|
33
|
+
|
|
34
|
+
_set(hostname, family, entries, lookups, hostnames)
|
|
35
|
+
|
|
36
|
+
@store[:lookups] = lookups
|
|
37
|
+
@store[:hostnames] = hostnames
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def evict(hostname, ip)
|
|
42
|
+
ip = ip.to_s
|
|
43
|
+
|
|
44
|
+
@store.transaction do
|
|
45
|
+
lookups = @store[:lookups] || EMPTY_HASH
|
|
46
|
+
hostnames = @store[:hostnames] || EMPTY
|
|
47
|
+
|
|
48
|
+
_evict(hostname, ip, lookups, hostnames)
|
|
49
|
+
|
|
50
|
+
@store[:lookups] = lookups
|
|
51
|
+
@store[:hostnames] = hostnames
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -512,12 +512,16 @@ module HTTPX
|
|
|
512
512
|
transition(:open)
|
|
513
513
|
end
|
|
514
514
|
|
|
515
|
+
# moves the resolver state machine to the +nextstate+ state (if all conditions are met)-
|
|
515
516
|
def transition(nextstate)
|
|
516
517
|
case nextstate
|
|
517
518
|
when :idle
|
|
518
|
-
if @io
|
|
519
|
-
@io.close
|
|
519
|
+
if (io = @io)
|
|
520
520
|
@io = nil
|
|
521
|
+
io.close
|
|
522
|
+
|
|
523
|
+
# @fiber-switch-guard
|
|
524
|
+
return if @io
|
|
521
525
|
end
|
|
522
526
|
when :open
|
|
523
527
|
return unless @state == :idle
|
|
@@ -531,7 +535,14 @@ module HTTPX
|
|
|
531
535
|
when :closed
|
|
532
536
|
return if @state == :closed
|
|
533
537
|
|
|
534
|
-
|
|
538
|
+
if (io = @io)
|
|
539
|
+
@io = nil
|
|
540
|
+
io.close
|
|
541
|
+
|
|
542
|
+
# @fiber-switch-guard
|
|
543
|
+
return if @io
|
|
544
|
+
end
|
|
545
|
+
|
|
535
546
|
@start_timeout = nil
|
|
536
547
|
@write_buffer.clear
|
|
537
548
|
@read_buffer.clear
|