restify 1.13.0 → 1.15.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +98 -6
  3. data/lib/restify/adapter/em.rb +6 -13
  4. data/lib/restify/adapter/pooled_em.rb +35 -40
  5. data/lib/restify/adapter/typhoeus.rb +57 -51
  6. data/lib/restify/context.rb +5 -9
  7. data/lib/restify/error.rb +24 -0
  8. data/lib/restify/global.rb +1 -0
  9. data/lib/restify/logging.rb +1 -1
  10. data/lib/restify/processors/base/parsing.rb +5 -9
  11. data/lib/restify/processors/base.rb +2 -6
  12. data/lib/restify/promise.rb +1 -3
  13. data/lib/restify/request.rb +13 -5
  14. data/lib/restify/resource.rb +2 -2
  15. data/lib/restify/response.rb +0 -2
  16. data/lib/restify/timeout.rb +1 -3
  17. data/lib/restify/version.rb +3 -3
  18. data/spec/restify/cache_spec.rb +2 -2
  19. data/spec/restify/context_spec.rb +10 -7
  20. data/spec/restify/error_spec.rb +10 -0
  21. data/spec/restify/features/head_requests_spec.rb +7 -7
  22. data/spec/restify/features/request_bodies_spec.rb +84 -0
  23. data/spec/restify/features/request_errors_spec.rb +19 -0
  24. data/spec/restify/features/request_headers_spec.rb +16 -17
  25. data/spec/restify/features/response_errors_spec.rb +127 -0
  26. data/spec/restify/global_spec.rb +6 -6
  27. data/spec/restify/link_spec.rb +9 -9
  28. data/spec/restify/processors/base_spec.rb +1 -0
  29. data/spec/restify/processors/json_spec.rb +2 -1
  30. data/spec/restify/processors/msgpack_spec.rb +8 -7
  31. data/spec/restify/promise_spec.rb +8 -4
  32. data/spec/restify/registry_spec.rb +2 -2
  33. data/spec/restify/relation_spec.rb +18 -17
  34. data/spec/restify/resource_spec.rb +9 -8
  35. data/spec/restify/timeout_spec.rb +4 -4
  36. data/spec/restify_spec.rb +52 -57
  37. data/spec/spec_helper.rb +11 -8
  38. data/spec/support/stub_server.rb +106 -0
  39. metadata +30 -23
  40. data/spec/restify/features/response_errors.rb +0 -79
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e2b5dacc128de6cbfed604453544f51eaeffd16b9b6cb83f5353ecca66d24a8d
4
- data.tar.gz: 5c3eba335362aea288f851327fa96262e5b464b9a747b092955241bb362e9c0d
3
+ metadata.gz: c29c95a9b1c0b18f2a73b51c99da7cdddf163e1cee33c43272810f5557b1375b
4
+ data.tar.gz: 95f956684154058cc6f25b7d4ffebde6706864b00c1369e91c8f8b5fcc50f0d9
5
5
  SHA512:
6
- metadata.gz: 32569c3544a1ac6734b3c81c1e2b6a46ea7dba324c1f71f5e85bd1df93b8b73bb70114a7cec37dd8e38e561a821e674e8c437c78757d78df75e1e51b1a5a79f8
7
- data.tar.gz: 6f03d156fcc20e0dfba3d7fda0a5c7851ff5d9342fbb9b50c4d90d1f43bf65e92f0ecabb714c4023ce4ca8cfb95b231abd7ea5d1e05339e839ea046b17f3d1fe
6
+ metadata.gz: f420cc4d0c4ebbdade1bc1a780c24218b5eea707bd8e420c113d155d58067936464dc3d4807717de8f515ed586784c9f13476b46c66f68c8d1181501937db3a2
7
+ data.tar.gz: 71da11cdf50d25abc968af02c6157c63f1aa5703c88f7ae28c3394aa028d9f038013d220283510532f7b540dffc69d0abad5fe4952c4e27df4910a4009475ae5
data/CHANGELOG.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # Changelog
2
+
2
3
  All notable changes to this project will be documented in this file.
3
4
 
4
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
5
6
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6
7
 
7
8
 
8
-
9
9
  ## Unreleased
10
10
  ---
11
11
 
@@ -18,135 +18,227 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
18
18
  ### Breaks
19
19
 
20
20
 
21
+ ## 1.15.2 - (2021-12-23)
22
+
23
+ ---
24
+
25
+ ### Fixes
26
+
27
+ - ActiveSupport v7.0 issues with cache module
28
+
29
+
30
+ ## 1.15.1 - (2021-07-15)
31
+
32
+ ---
33
+
34
+ ### Fixes
35
+
36
+ - Typhoeus internal exception when request timed out
37
+
38
+ ## 1.15.0 - (2021-07-09)
39
+
40
+ ---
41
+
42
+ ### New
43
+
44
+ - Improve memory usage when running lots of requests with typhoeus adapter
45
+ - Use hydra for synchronous requests
46
+ - Increased thread stability of typhoeus adapter (new internal queuing mechanism)
47
+
48
+ ### Changes
49
+
50
+ - Use Ruby 2.5 as baseline for testing and linting
51
+ - Add Ruby 3.0 to automated testing
52
+ - Changed timing behavior for multiple requests due to new internal queuing mechanism for the typhoeus adapter
53
+
54
+ ## 1.14.0 - (2020-12-15)
55
+
56
+ ---
57
+
58
+ ### New
59
+
60
+ - Allow making requests with non-JSON bodies and custom content types (#42)
61
+
21
62
  ## 1.13.0 - (2020-06-12)
63
+
22
64
  ---
23
65
 
24
66
  ### New
25
- * typhoeus: Support setting per-request libcurl options on adapter
26
- * typhoeus: Enable short TCP keepalive probes by default (5s/5s)
27
67
 
68
+ - typhoeus: Support setting per-request libcurl options on adapter
69
+ - typhoeus: Enable short TCP keepalive probes by default (5s/5s)
28
70
 
29
71
  ## 1.12.0 - (2020-04-01)
72
+
30
73
  ---
31
74
 
32
75
  ### Added
33
- * Explicit exception class for HTTP status code 410 (Gone)
76
+
77
+ - Explicit exception class for HTTP status code 410 (Gone)
34
78
 
35
79
  ### Changed
36
80
 
37
81
  ### Fixed
38
- * `GatewayError` exception classes introduced in v1.11.0 now properly inherit from `ServerError` (#30)
39
82
 
83
+ - `GatewayError` exception classes introduced in v1.11.0 now properly inherit from `ServerError` (#30)
40
84
 
41
85
  ## 1.11.0 - (2019-07-11)
86
+
42
87
  ### Added
43
- * Explicit exception classes for HTTP status codes 500, 502, 503, 504
88
+
89
+ - Explicit exception classes for HTTP status codes 500, 502, 503, 504
44
90
 
45
91
  ## 1.10.0 - 2018-12-11
92
+
46
93
  ### Changed
94
+
47
95
  - Raise more specific error on a few status codes (#17)
48
96
  - Complete promises with an empty list (but a list) of dependencies (#18)
49
97
 
50
98
  ## 1.9.0 - 2018-11-13
99
+
51
100
  ### Changed
101
+
52
102
  - Do not raise error on 3XX responses but return responses
53
103
 
54
104
  ## 1.8.0 - 2018-08-22
105
+
55
106
  ### Added
107
+
56
108
  - Add HEAD request method (#16)
57
109
 
58
110
  ## 1.7.0 - 2018-08-15
111
+
59
112
  ### Added
113
+
60
114
  - Introduce promise dependency timeouts (#15)
61
115
 
62
116
  ## 1.6.0 - 2018-08-09
117
+
63
118
  ### Changed
119
+
64
120
  - Specify headers on restify clients and individual requests (#14)
65
121
 
66
122
  ## 1.5.0 - 2018-07-31
123
+
67
124
  ### Added
125
+
68
126
  - Add MessagePack processor enabled by default
69
127
 
70
128
  ### Changed
129
+
71
130
  - Tune typhoeus adapter to be more race-condition resilent
72
131
 
73
132
  ## 1.4.4 - 2018-07-13
133
+
74
134
  ### Added
135
+
75
136
  - Add `#request` to `NetworkError` to ease debugging
76
137
 
77
138
  ### Changed
139
+
78
140
  - Fix race condition in typhoeus adapter
79
141
 
80
142
  ## 1.4.3 - 2017-11-15
143
+
81
144
  ### Added
145
+
82
146
  - Add advanced logging capabilities using logging gem
83
147
 
84
148
  ### Changed
149
+
85
150
  - Improve compatibility with webmocks returning `nil` as headers
86
151
 
87
152
  ## 1.4.1 - 2017-11-15
153
+
88
154
  ### Changed
155
+
89
156
  - Fix possible deadlock issues
90
157
 
91
158
  ## 1.4.0 - 2017-11-10
159
+
92
160
  ### Added
161
+
93
162
  - Add timeout option to requests (only supported by typhoeus adapter)
94
163
 
95
164
  ### Changed
165
+
96
166
  - Fix possible concurrency issue with typhoeus adapter
97
167
 
98
168
  ## 1.3.1 - 2017-11-10
169
+
99
170
  ### Changed
171
+
100
172
  - Improve typhoeus adapters initial request queuing
101
173
  - Disable default pipelining
102
174
 
103
175
  ## 1.3.0 - 2017-11-08
176
+
104
177
  ### Changed
178
+
105
179
  - Improve typhoeus adapter to better utilize concurrency
106
180
  - Default to new typhoeus adapter
107
181
 
108
182
  ## 1.2.1 - 2017-10-30
183
+
109
184
  ### Changed
185
+
110
186
  - Fix issue with Ruby 2.2 compatibility
111
187
 
112
188
  ## 1.2.0 - 2017-10-30
189
+
113
190
  ### Added
191
+
114
192
  - Add experimental PooledEM adapter (#10)
115
193
 
116
194
  ### Changed
195
+
117
196
  - Improve marshaling of resources
118
197
 
119
198
  ## 1.1.0 - 2017-05-12
199
+
120
200
  ### Added
201
+
121
202
  - Add shortcuts for creating fulfilled / rejected promises (#6)
122
203
 
123
204
  ### Changed
205
+
124
206
  - Return response body if no processor matches (#7)
125
207
 
126
208
  ## 1.0.0 - 2016-08-22
209
+
127
210
  ### Added
211
+
128
212
  - Experimental cache API doing nothing for now
129
213
 
130
214
  ### Changed
215
+
131
216
  - Use `~> 1.0` of `concurrent-ruby`
132
217
 
133
218
  ## 0.5.0 - 2016-04-04
219
+
134
220
  ### Added
221
+
135
222
  - Add `sync` option to typhoeus adapter
136
223
  - Add registry for storing entry points
137
224
 
138
225
  ### Changed
226
+
139
227
  - Make eventmachine based adapter default
140
228
 
141
229
  ## 0.4.0 - 2016-02-24
230
+
142
231
  ### Added
232
+
143
233
  - Add method to explicit access resource data
144
234
 
145
235
  ### Changed
236
+
146
237
  - Use typhoeus as default adapter
147
238
  - `Restify.new` returns relation now instead of resource
148
239
 
149
240
  ### Removed
241
+
150
242
  - Drop obligation in favor of simple Concurrent::IVar based promise class.
151
243
  Notable changes:
152
244
  - Returned object us of type `Restify::Promise` now.
@@ -29,7 +29,7 @@ module Restify
29
29
  end
30
30
 
31
31
  # rubocop:disable Style/IdenticalConditionalBranches
32
- def call(request, writer, retried = false)
32
+ def call(request, writer, retried: false)
33
33
  if requests.empty?
34
34
  requests << [request, writer, retried]
35
35
  process_next
@@ -47,10 +47,6 @@ module Restify
47
47
  @pipeline
48
48
  end
49
49
 
50
- # rubocop:disable Metrics/AbcSize
51
- # rubocop:disable Metrics/CyclomaticComplexity
52
- # rubocop:disable Metrics/MethodLength
53
- # rubocop:disable Metrics/PerceivedComplexity
54
50
  def process_next
55
51
  return if requests.empty?
56
52
 
@@ -77,7 +73,7 @@ module Restify
77
73
  req.last_effective_url,
78
74
  req.response_header.status,
79
75
  req.response_header,
80
- req.response
76
+ req.response,
81
77
  )
82
78
 
83
79
  if req.response_header['CONNECTION'] == 'close'
@@ -108,7 +104,6 @@ module Restify
108
104
  end
109
105
  end
110
106
  end
111
- # rubocop:enable all
112
107
  end
113
108
 
114
109
  def call_native(request, writer)
@@ -128,12 +123,10 @@ module Restify
128
123
  return if EventMachine.reactor_running?
129
124
 
130
125
  Thread.new do
131
- begin
132
- EventMachine.run {}
133
- rescue StandardError => e
134
- puts "#{self.class} -> #{e}\n#{e.backtrace.join("\n")}"
135
- raise e
136
- end
126
+ EventMachine.run
127
+ rescue StandardError => e
128
+ puts "#{self.class} -> #{e}\n#{e.backtrace.join("\n")}"
129
+ raise e
137
130
  end
138
131
  end
139
132
  end
@@ -63,7 +63,7 @@ module Restify
63
63
  #
64
64
  def release(conn)
65
65
  @available.unshift(conn) if @available.size < @size
66
- @used -= 1 if @used > 0
66
+ @used -= 1 if @used.positive?
67
67
 
68
68
  logger.debug do
69
69
  "[#{conn.uri}] Released to pool (#{@available.size}/#{@used}/#{size})"
@@ -97,7 +97,7 @@ module Restify
97
97
  private
98
98
 
99
99
  def close(conn)
100
- @used -= 1 if @used > 0
100
+ @used -= 1 if @used.positive?
101
101
  @host[conn.uri.to_s] -= 1
102
102
 
103
103
  conn.close
@@ -164,7 +164,7 @@ module Restify
164
164
  def new(origin)
165
165
  logger.debug do
166
166
  "Connect to '#{origin}' " \
167
- "(#{@connect_timeout}/#{@inactivity_timeout})..."
167
+ "(#{@connect_timeout}/#{@inactivity_timeout})..."
168
168
  end
169
169
 
170
170
  @host[origin] += 1
@@ -197,11 +197,10 @@ module Restify
197
197
  end
198
198
 
199
199
  def initialize(**kwargs)
200
+ super()
200
201
  @pool = Pool.new(**kwargs)
201
202
  end
202
203
 
203
- # rubocop:disable Metrics/MethodLength
204
- # rubocop:disable Metrics/AbcSize
205
204
  # rubocop:disable Metrics/BlockLength
206
205
  def call_native(request, writer)
207
206
  next_tick do
@@ -212,39 +211,37 @@ module Restify
212
211
  end
213
212
 
214
213
  defer.callback do |conn|
215
- begin
216
- req = conn.send request.method.downcase,
217
- keepalive: true,
218
- redirects: 3,
219
- path: request.uri.normalized_path,
220
- query: request.uri.normalized_query,
221
- body: request.body,
222
- head: request.headers
223
-
224
- req.callback do
225
- writer.fulfill Response.new(
226
- request,
227
- req.last_effective_url,
228
- req.response_header.status,
229
- req.response_header,
230
- req.response
231
- )
232
-
233
- if req.response_header['CONNECTION'] == 'close'
234
- @pool.remove(conn)
235
- else
236
- @pool << conn
237
- end
238
- end
239
-
240
- req.errback do
214
+ req = conn.send request.method.downcase,
215
+ keepalive: true,
216
+ redirects: 3,
217
+ path: request.uri.normalized_path,
218
+ query: request.uri.normalized_query,
219
+ body: request.body,
220
+ head: request.headers
221
+
222
+ req.callback do
223
+ writer.fulfill Response.new(
224
+ request,
225
+ req.last_effective_url,
226
+ req.response_header.status,
227
+ req.response_header,
228
+ req.response,
229
+ )
230
+
231
+ if req.response_header['CONNECTION'] == 'close'
241
232
  @pool.remove(conn)
242
- writer.reject(req.error)
233
+ else
234
+ @pool << conn
243
235
  end
244
- rescue Exception => e # rubocop:disable Lint/RescueException
236
+ end
237
+
238
+ req.errback do
245
239
  @pool.remove(conn)
246
- writer.reject(e)
240
+ writer.reject(req.error)
247
241
  end
242
+ rescue Exception => e # rubocop:disable Lint/RescueException
243
+ @pool.remove(conn)
244
+ writer.reject(e)
248
245
  end
249
246
  end
250
247
  end
@@ -261,12 +258,10 @@ module Restify
261
258
  return if EventMachine.reactor_running?
262
259
 
263
260
  Thread.new do
264
- begin
265
- EventMachine.run {}
266
- rescue StandardError => e
267
- logger.error(e)
268
- raise e
269
- end
261
+ EventMachine.run
262
+ rescue StandardError => e
263
+ logger.error(e)
264
+ raise e
270
265
  end
271
266
  end
272
267
  end
@@ -13,60 +13,52 @@ module Restify
13
13
 
14
14
  DEFAULT_HEADERS = {
15
15
  'Expect' => '',
16
- 'Transfer-Encoding' => ''
16
+ 'Transfer-Encoding' => '',
17
17
  }.freeze
18
18
 
19
19
  DEFAULT_OPTIONS = {
20
20
  followlocation: true,
21
21
  tcp_keepalive: true,
22
22
  tcp_keepidle: 5,
23
- tcp_keepintvl: 5
23
+ tcp_keepintvl: 5,
24
24
  }.freeze
25
25
 
26
26
  def initialize(sync: false, options: {}, **kwargs)
27
- @sync = sync
28
27
  @hydra = ::Typhoeus::Hydra.new(**kwargs)
29
28
  @mutex = Mutex.new
30
- @signal = ConditionVariable.new
31
- @thread = nil
32
29
  @options = DEFAULT_OPTIONS.merge(options)
30
+ @queue = Queue.new
31
+ @sync = sync
32
+ @thread = nil
33
+
34
+ super()
33
35
  end
34
36
 
35
37
  def sync?
36
38
  @sync
37
39
  end
38
40
 
39
- # rubocop:disable Metrics/AbcSize
40
- # rubocop:disable Metrics/MethodLength
41
41
  def call_native(request, writer)
42
42
  req = convert(request, writer)
43
43
 
44
44
  if sync?
45
- req.run
45
+ @hydra.queue(req)
46
+ @hydra.run
46
47
  else
47
- @mutex.synchronize do
48
- debug 'request:add',
49
- tag: request.object_id,
50
- method: request.method.upcase,
51
- url: request.uri
52
-
53
- @hydra.queue(req)
54
- @hydra.dequeue_many
55
-
56
- thread.run unless thread.status
57
- end
48
+ debug 'request:add',
49
+ tag: request.object_id,
50
+ method: request.method.upcase,
51
+ url: request.uri,
52
+ timeout: request.timeout
58
53
 
59
- debug 'request:signal', tag: request.object_id
54
+ @queue << convert(request, writer)
60
55
 
61
- @signal.signal
56
+ thread.run unless thread.status
62
57
  end
63
58
  end
64
- # rubocop:enable all
65
59
 
66
60
  private
67
61
 
68
- # rubocop:disable Metrics/AbcSize
69
- # rubocop:disable Metrics/MethodLength
70
62
  def convert(request, writer)
71
63
  ::Typhoeus::Request.new(
72
64
  request.uri,
@@ -75,25 +67,28 @@ module Restify
75
67
  headers: DEFAULT_HEADERS.merge(request.headers),
76
68
  body: request.body,
77
69
  timeout: request.timeout,
78
- connecttimeout: request.timeout
70
+ connecttimeout: request.timeout,
79
71
  ).tap do |req|
80
72
  req.on_complete do |response|
81
73
  debug 'request:complete',
82
74
  tag: request.object_id,
83
- status: response.code
75
+ status: response.code,
76
+ message: response.return_message,
77
+ timeout: response.timed_out?
84
78
 
85
- if response.timed_out?
86
- writer.reject Restify::Timeout.new request
87
- elsif response.code.zero?
79
+ if response.timed_out? || response.code.zero?
88
80
  writer.reject \
89
81
  Restify::NetworkError.new(request, response.return_message)
90
82
  else
91
83
  writer.fulfill convert_back(response, request)
92
84
  end
85
+
86
+ # Add all newly queued requests to active hydra, e.g. requests
87
+ # queued in a completion callback.
88
+ dequeue_all
93
89
  end
94
90
  end
95
91
  end
96
- # rubocop:enable all
97
92
 
98
93
  def convert_back(response, request)
99
94
  uri = request.uri
@@ -117,38 +112,49 @@ module Restify
117
112
  # Recreate thread if nil or dead
118
113
  debug 'hydra:spawn'
119
114
 
120
- @thread = Thread.new { _loop }
115
+ @thread = Thread.new do
116
+ Thread.current.name = 'Restify/Typhoeus Background'
117
+ run
118
+ end
121
119
  end
122
120
 
123
121
  @thread
124
122
  end
125
123
 
126
- def _loop
127
- Thread.current.name = 'Restify/Typhoeus Background'
128
- loop { _run }
129
- end
124
+ def run
125
+ runs = 0
130
126
 
131
- def _ongoing?
132
- @hydra.queued_requests.any? || @hydra.multi.easy_handles.any?
133
- end
127
+ loop do
128
+ if @queue.empty? && runs > 100
129
+ debug 'hydra:gc'
130
+ GC.start(full_mark: false, immediate_sweep: false)
131
+ runs = 0
132
+ end
133
+
134
+ debug 'hydra:pop'
134
135
 
135
- # rubocop:disable Metrics/MethodLength
136
- def _run
137
- debug 'hydra:run'
138
- @hydra.run while _ongoing?
139
- debug 'hydra:completed'
136
+ # Wait for next item and add all available requests to hydra
137
+ @hydra.queue @queue.pop
138
+ dequeue_all
140
139
 
141
- @mutex.synchronize do
142
- return if _ongoing?
140
+ debug 'hydra:run'
141
+ @hydra.run
142
+ runs += 1
143
+ debug 'hydra:completed'
144
+ rescue StandardError => e
145
+ logger.error(e)
146
+ end
147
+ ensure
148
+ debug 'hydra:exit'
149
+ end
143
150
 
144
- debug 'hydra:pause'
145
- @signal.wait(@mutex, 60)
146
- debug 'hydra:resumed'
151
+ def dequeue_all
152
+ loop do
153
+ @hydra.queue @queue.pop(true)
154
+ rescue ThreadError
155
+ break
147
156
  end
148
- rescue StandardError => e
149
- logger.error(e)
150
157
  end
151
- # rubocop:enable all
152
158
 
153
159
  def _log_prefix
154
160
  "[#{object_id}/#{Thread.current.object_id}]"
@@ -47,26 +47,22 @@ module Restify
47
47
  processor.new(context, response).resource
48
48
  end
49
49
 
50
- # rubocop:disable Metrics/MethodLength
51
50
  def request(method, uri, data: nil, headers: {}, **kwargs)
52
51
  request = Request.new(
53
52
  headers: default_headers.merge(headers),
54
53
  **kwargs,
55
54
  method: method,
56
55
  uri: join(uri),
57
- data: data
56
+ data: data,
58
57
  )
59
58
 
60
59
  ret = cache.call(request) {|req| adapter.call(req) }
61
60
  ret.then do |response|
62
- if !response.errored?
63
- process response
64
- else
65
- raise ResponseError.from_code(response)
66
- end
61
+ raise ResponseError.from_code(response) if response.errored?
62
+
63
+ process(response)
67
64
  end
68
65
  end
69
- # rubocop:enable all
70
66
 
71
67
  def encode_with(coder)
72
68
  coder.map = marshal_dump
@@ -79,7 +75,7 @@ module Restify
79
75
  def marshal_dump
80
76
  {
81
77
  uri: uri.to_s,
82
- headers: default_headers
78
+ headers: default_headers,
83
79
  }
84
80
  end
85
81
 
data/lib/restify/error.rb CHANGED
@@ -34,6 +34,8 @@ module Restify
34
34
  Gone.new(response)
35
35
  when 422
36
36
  UnprocessableEntity.new(response)
37
+ when 429
38
+ TooManyRequests.new(response)
37
39
  when 400...500
38
40
  ClientError.new(response)
39
41
  when 500
@@ -110,15 +112,37 @@ module Restify
110
112
  # This makes it easy to rescue specific expected error types.
111
113
 
112
114
  class BadRequest < ClientError; end
115
+
113
116
  class Unauthorized < ClientError; end
117
+
114
118
  class NotFound < ClientError; end
119
+
115
120
  class NotAcceptable < ClientError; end
121
+
116
122
  class Gone < ClientError; end
123
+
117
124
  class UnprocessableEntity < ClientError; end
118
125
 
126
+ class TooManyRequests < ClientError
127
+ def retry_after
128
+ case response.headers['RETRY_AFTER']
129
+ when /^\d+$/
130
+ DateTime.now + Rational(response.headers['RETRY_AFTER'].to_i, 86_400)
131
+ when String
132
+ begin
133
+ DateTime.httpdate response.headers['RETRY_AFTER']
134
+ rescue ArgumentError
135
+ nil
136
+ end
137
+ end
138
+ end
139
+ end
140
+
119
141
  class InternalServerError < ServerError; end
120
142
 
121
143
  class BadGateway < GatewayError; end
144
+
122
145
  class ServiceUnavailable < GatewayError; end
146
+
123
147
  class GatewayTimeout < GatewayError; end
124
148
  end