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.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_7_8.md +5 -0
  3. data/doc/release_notes/1_8_0.md +100 -0
  4. data/lib/httpx/adapters/datadog.rb +3 -1
  5. data/lib/httpx/connection/http1.rb +10 -1
  6. data/lib/httpx/connection/http2.rb +37 -4
  7. data/lib/httpx/connection.rb +76 -7
  8. data/lib/httpx/errors.rb +8 -1
  9. data/lib/httpx/io/tcp.rb +11 -1
  10. data/lib/httpx/options.rb +16 -4
  11. data/lib/httpx/parser/http1.rb +8 -2
  12. data/lib/httpx/plugins/auth.rb +52 -4
  13. data/lib/httpx/plugins/{response_cache → cache}/file_store.rb +1 -1
  14. data/lib/httpx/plugins/{response_cache → cache}/store.rb +1 -1
  15. data/lib/httpx/plugins/cache.rb +221 -0
  16. data/lib/httpx/plugins/fiber_concurrency.rb +50 -3
  17. data/lib/httpx/plugins/ntlm_v2_auth.rb +92 -0
  18. data/lib/httpx/plugins/oauth.rb +66 -14
  19. data/lib/httpx/plugins/proxy.rb +5 -0
  20. data/lib/httpx/plugins/response_cache.rb +26 -105
  21. data/lib/httpx/plugins/retries.rb +13 -5
  22. data/lib/httpx/plugins/server_sent_events.rb +158 -0
  23. data/lib/httpx/plugins/ssrf_filter.rb +16 -1
  24. data/lib/httpx/plugins/stream.rb +7 -3
  25. data/lib/httpx/plugins/tracing.rb +15 -4
  26. data/lib/httpx/request.rb +18 -1
  27. data/lib/httpx/resolver/cache/file.rb +56 -0
  28. data/lib/httpx/resolver/native.rb +14 -3
  29. data/lib/httpx/response/body.rb +4 -2
  30. data/lib/httpx/response.rb +9 -1
  31. data/lib/httpx/selector.rb +7 -1
  32. data/lib/httpx/version.rb +1 -1
  33. data/sig/chainable.rbs +3 -0
  34. data/sig/connection/http1.rbs +1 -1
  35. data/sig/connection/http2.rbs +1 -1
  36. data/sig/connection.rbs +11 -8
  37. data/sig/errors.rbs +9 -3
  38. data/sig/httpx.rbs +2 -0
  39. data/sig/io/tcp.rbs +2 -0
  40. data/sig/loggable.rbs +4 -0
  41. data/sig/options.rbs +25 -12
  42. data/sig/parser/http1.rbs +3 -1
  43. data/sig/plugins/auth/ntlm.rbs +1 -1
  44. data/sig/plugins/{response_cache → cache}/file_store.rbs +2 -2
  45. data/sig/plugins/{response_cache → cache}/store.rbs +2 -2
  46. data/sig/plugins/cache.rbs +69 -0
  47. data/sig/plugins/fiber_concurrency.rbs +4 -0
  48. data/sig/plugins/ntlm_v2_auth.rbs +36 -0
  49. data/sig/plugins/response_cache.rbs +13 -38
  50. data/sig/plugins/retries.rbs +5 -5
  51. data/sig/plugins/server_sent_events.rbs +45 -0
  52. data/sig/plugins/ssrf_filter.rbs +5 -1
  53. data/sig/plugins/stream.rbs +1 -1
  54. data/sig/plugins/stream_bidi.rbs +0 -2
  55. data/sig/plugins/webdav.rbs +1 -1
  56. data/sig/pool.rbs +2 -2
  57. data/sig/request.rbs +7 -3
  58. data/sig/resolver/cache/file.rbs +13 -0
  59. data/sig/resolver/entry.rbs +1 -1
  60. data/sig/resolver/https.rbs +3 -3
  61. data/sig/resolver/multi.rbs +1 -1
  62. data/sig/resolver/native.rbs +5 -5
  63. data/sig/resolver/resolver.rbs +1 -3
  64. data/sig/resolver/system.rbs +2 -2
  65. data/sig/resolver.rbs +3 -0
  66. data/sig/response.rbs +3 -0
  67. data/sig/selector.rbs +11 -8
  68. data/sig/timers.rbs +5 -5
  69. data/sig/transcoder/body.rbs +1 -1
  70. data/sig/transcoder/gzip.rbs +3 -2
  71. data/sig/transcoder/multipart.rbs +4 -1
  72. data/sig/transcoder/utils/deflater.rbs +2 -0
  73. data/sig/transcoder.rbs +2 -0
  74. data/sig/utils.rbs +1 -1
  75. metadata +19 -7
@@ -3,7 +3,8 @@
3
3
  module HTTPX
4
4
  module Plugins
5
5
  #
6
- # This plugin adds support for retrying requests when certain errors happen.
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
- require_relative "response_cache/store"
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
- elsif request.cacheable_verb? && ResponseCache.cacheable_response?(response)
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
- cached_response = request.options.response_cache_store.get(request)
88
+ super
134
89
 
135
- return unless cached_response && match_by_vary?(request, cached_response)
90
+ return if request.response # already cached
136
91
 
137
- cached_response.body.rewind
92
+ cached_response = retrieve_cached_response(request)
138
93
 
139
- if cached_response.fresh?
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
- request.cacheable_verb? &&
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
- # whether the +response+ complies with the directives set by the +request+ "vary" header
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, response)
168
- vary = response.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 = response.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 :original_request, :revalidated_at
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
- 2 * (offset - 1)
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) * 2
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!(&SsrfFilter.method(:unsafe_ip_address?))
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
 
@@ -46,8 +46,8 @@ module HTTPX
46
46
  end
47
47
  end
48
48
 
49
- def each_line
50
- return enum_for(__method__) unless block_given?
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
- yield line.byteslice(0..(idx - 1))
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 send(request)
115
- # request init time is only the same as the connection init time
116
- # if the connection is going through the connection handshake.
117
- request.init_time ||= @init_time unless open?
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
- @io.close if @io
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