restify 1.14.0 → 1.15.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/CHANGELOG.md +14 -0
- data/lib/restify/adapter/em.rb +4 -6
- data/lib/restify/adapter/pooled_em.rb +33 -37
- data/lib/restify/adapter/typhoeus.rb +54 -41
- data/lib/restify/context.rb +3 -3
- data/lib/restify/error.rb +24 -0
- data/lib/restify/processors/base.rb +2 -6
- data/lib/restify/processors/base/parsing.rb +2 -6
- data/lib/restify/promise.rb +1 -3
- data/lib/restify/resource.rb +1 -1
- data/lib/restify/timeout.rb +1 -3
- data/lib/restify/version.rb +1 -1
- data/spec/restify/features/head_requests_spec.rb +5 -6
- data/spec/restify/features/request_bodies_spec.rb +8 -11
- data/spec/restify/features/request_headers_spec.rb +6 -7
- data/spec/restify/features/response_errors_spec.rb +58 -10
- data/spec/restify/global_spec.rb +1 -1
- data/spec/restify_spec.rb +50 -55
- data/spec/spec_helper.rb +3 -7
- data/spec/support/stub_server.rb +102 -0
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 241fd43c8e40d2a0c2c839443df03106edf3c8e491cac9338277ff4327a3927f
|
|
4
|
+
data.tar.gz: d8eea64a1de42ee6eb4666feb510669b69482f24c72a0f5c883f3482143751ca
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 603395d24a3ecad6d35b217ef2d154cd0982f9cd9181769b2a5aa949e134f0f9af1a15cc731f54a2d700e3bd1162b667240420e5669af20b48c66a1ba84fc2c5
|
|
7
|
+
data.tar.gz: 89e64dcd00dde5a7711922619380d2003db5f8c42a7cbc473b19678b6ed251134ffe4b4c94b86afaa7efa7dea030c6d43876e57c00c85ea76523f9f7029f6804
|
data/CHANGELOG.md
CHANGED
|
@@ -18,6 +18,20 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|
|
18
18
|
### Breaks
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
## 1.15.0 - (2021-07-09)
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
### New
|
|
25
|
+
* Improve memory usage when running lots of requests with typhoeus adapter
|
|
26
|
+
* Use hydra for synchronous requests
|
|
27
|
+
* Increased thread stability of typhoeus adapter (new internal queuing mechanism)
|
|
28
|
+
|
|
29
|
+
### Changes
|
|
30
|
+
* Use Ruby 2.5 as baseline for testing and linting
|
|
31
|
+
* Add Ruby 3.0 to automated testing
|
|
32
|
+
* Changed timing behavior for multiple requests due to new internal queuing mechanism for the typhoeus adapter
|
|
33
|
+
|
|
34
|
+
|
|
21
35
|
## 1.14.0 - (2020-12-15)
|
|
22
36
|
|
|
23
37
|
### New
|
data/lib/restify/adapter/em.rb
CHANGED
|
@@ -128,12 +128,10 @@ module Restify
|
|
|
128
128
|
return if EventMachine.reactor_running?
|
|
129
129
|
|
|
130
130
|
Thread.new do
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
raise e
|
|
136
|
-
end
|
|
131
|
+
EventMachine.run {}
|
|
132
|
+
rescue StandardError => e
|
|
133
|
+
puts "#{self.class} -> #{e}\n#{e.backtrace.join("\n")}"
|
|
134
|
+
raise e
|
|
137
135
|
end
|
|
138
136
|
end
|
|
139
137
|
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
|
|
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
|
|
100
|
+
@used -= 1 if @used.positive?
|
|
101
101
|
@host[conn.uri.to_s] -= 1
|
|
102
102
|
|
|
103
103
|
conn.close
|
|
@@ -212,39 +212,37 @@ module Restify
|
|
|
212
212
|
end
|
|
213
213
|
|
|
214
214
|
defer.callback do |conn|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
215
|
+
req = conn.send request.method.downcase,
|
|
216
|
+
keepalive: true,
|
|
217
|
+
redirects: 3,
|
|
218
|
+
path: request.uri.normalized_path,
|
|
219
|
+
query: request.uri.normalized_query,
|
|
220
|
+
body: request.body,
|
|
221
|
+
head: request.headers
|
|
222
|
+
|
|
223
|
+
req.callback do
|
|
224
|
+
writer.fulfill Response.new(
|
|
225
|
+
request,
|
|
226
|
+
req.last_effective_url,
|
|
227
|
+
req.response_header.status,
|
|
228
|
+
req.response_header,
|
|
229
|
+
req.response
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
if req.response_header['CONNECTION'] == 'close'
|
|
241
233
|
@pool.remove(conn)
|
|
242
|
-
|
|
234
|
+
else
|
|
235
|
+
@pool << conn
|
|
243
236
|
end
|
|
244
|
-
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
req.errback do
|
|
245
240
|
@pool.remove(conn)
|
|
246
|
-
writer.reject(
|
|
241
|
+
writer.reject(req.error)
|
|
247
242
|
end
|
|
243
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
244
|
+
@pool.remove(conn)
|
|
245
|
+
writer.reject(e)
|
|
248
246
|
end
|
|
249
247
|
end
|
|
250
248
|
end
|
|
@@ -261,12 +259,10 @@ module Restify
|
|
|
261
259
|
return if EventMachine.reactor_running?
|
|
262
260
|
|
|
263
261
|
Thread.new do
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
raise e
|
|
269
|
-
end
|
|
262
|
+
EventMachine.run {}
|
|
263
|
+
rescue StandardError => e
|
|
264
|
+
logger.error(e)
|
|
265
|
+
raise e
|
|
270
266
|
end
|
|
271
267
|
end
|
|
272
268
|
end
|
|
@@ -24,44 +24,39 @@ module Restify
|
|
|
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
41
|
# rubocop:disable Metrics/MethodLength
|
|
41
42
|
def call_native(request, writer)
|
|
42
43
|
req = convert(request, writer)
|
|
43
44
|
|
|
44
45
|
if sync?
|
|
45
|
-
req
|
|
46
|
+
@hydra.queue(req)
|
|
47
|
+
@hydra.run
|
|
46
48
|
else
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
url: request.uri
|
|
52
|
-
|
|
53
|
-
@hydra.queue(req)
|
|
54
|
-
@hydra.dequeue_many
|
|
55
|
-
|
|
56
|
-
thread.run unless thread.status
|
|
57
|
-
end
|
|
49
|
+
debug 'request:add',
|
|
50
|
+
tag: request.object_id,
|
|
51
|
+
method: request.method.upcase,
|
|
52
|
+
url: request.uri
|
|
58
53
|
|
|
59
|
-
|
|
54
|
+
@queue << convert(request, writer)
|
|
60
55
|
|
|
61
|
-
|
|
56
|
+
thread.run unless thread.status
|
|
62
57
|
end
|
|
63
58
|
end
|
|
64
|
-
# rubocop:enable
|
|
59
|
+
# rubocop:enable Metrics/MethodLength
|
|
65
60
|
|
|
66
61
|
private
|
|
67
62
|
|
|
@@ -90,10 +85,15 @@ module Restify
|
|
|
90
85
|
else
|
|
91
86
|
writer.fulfill convert_back(response, request)
|
|
92
87
|
end
|
|
88
|
+
|
|
89
|
+
# Add all newly queued requests to active hydra, e.g. requests
|
|
90
|
+
# queued in a completion callback.
|
|
91
|
+
dequeue_all
|
|
93
92
|
end
|
|
94
93
|
end
|
|
95
94
|
end
|
|
96
|
-
# rubocop:enable
|
|
95
|
+
# rubocop:enable Metrics/MethodLength
|
|
96
|
+
# rubocop:enable Metrics/AbcSize
|
|
97
97
|
|
|
98
98
|
def convert_back(response, request)
|
|
99
99
|
uri = request.uri
|
|
@@ -117,38 +117,51 @@ module Restify
|
|
|
117
117
|
# Recreate thread if nil or dead
|
|
118
118
|
debug 'hydra:spawn'
|
|
119
119
|
|
|
120
|
-
@thread = Thread.new
|
|
120
|
+
@thread = Thread.new do
|
|
121
|
+
Thread.current.name = 'Restify/Typhoeus Background'
|
|
122
|
+
run
|
|
123
|
+
end
|
|
121
124
|
end
|
|
122
125
|
|
|
123
126
|
@thread
|
|
124
127
|
end
|
|
125
128
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
129
|
+
# rubocop:disable Metrics/MethodLength
|
|
130
|
+
def run
|
|
131
|
+
runs = 0
|
|
132
|
+
|
|
133
|
+
loop do
|
|
134
|
+
if @queue.empty? && runs > 100
|
|
135
|
+
debug 'hydra:gc'
|
|
136
|
+
GC.start(full_mark: false, immediate_sweep: false)
|
|
137
|
+
runs = 0
|
|
138
|
+
end
|
|
130
139
|
|
|
131
|
-
|
|
132
|
-
@hydra.queued_requests.any? || @hydra.multi.easy_handles.any?
|
|
133
|
-
end
|
|
140
|
+
debug 'hydra:pop'
|
|
134
141
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
@hydra.run while _ongoing?
|
|
139
|
-
debug 'hydra:completed'
|
|
142
|
+
# Wait for next item and add all available requests to hydra
|
|
143
|
+
@hydra.queue @queue.pop
|
|
144
|
+
dequeue_all
|
|
140
145
|
|
|
141
|
-
|
|
142
|
-
|
|
146
|
+
debug 'hydra:run'
|
|
147
|
+
@hydra.run
|
|
148
|
+
runs += 1
|
|
149
|
+
debug 'hydra:completed'
|
|
150
|
+
rescue StandardError => e
|
|
151
|
+
logger.error(e)
|
|
152
|
+
end
|
|
153
|
+
ensure
|
|
154
|
+
debug 'hydra:exit'
|
|
155
|
+
end
|
|
156
|
+
# rubocop:enable Metrics/MethodLength
|
|
143
157
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
158
|
+
def dequeue_all
|
|
159
|
+
loop do
|
|
160
|
+
@hydra.queue @queue.pop(true)
|
|
161
|
+
rescue ThreadError
|
|
162
|
+
break
|
|
147
163
|
end
|
|
148
|
-
rescue StandardError => e
|
|
149
|
-
logger.error(e)
|
|
150
164
|
end
|
|
151
|
-
# rubocop:enable all
|
|
152
165
|
|
|
153
166
|
def _log_prefix
|
|
154
167
|
"[#{object_id}/#{Thread.current.object_id}]"
|
data/lib/restify/context.rb
CHANGED
|
@@ -59,10 +59,10 @@ module Restify
|
|
|
59
59
|
|
|
60
60
|
ret = cache.call(request) {|req| adapter.call(req) }
|
|
61
61
|
ret.then do |response|
|
|
62
|
-
if
|
|
63
|
-
process response
|
|
64
|
-
else
|
|
62
|
+
if response.errored?
|
|
65
63
|
raise ResponseError.from_code(response)
|
|
64
|
+
else
|
|
65
|
+
process response
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
end
|
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
|
|
@@ -5,9 +5,7 @@ module Restify
|
|
|
5
5
|
class Base
|
|
6
6
|
extend Forwardable
|
|
7
7
|
|
|
8
|
-
attr_reader :context
|
|
9
|
-
|
|
10
|
-
attr_reader :response
|
|
8
|
+
attr_reader :context, :response
|
|
11
9
|
|
|
12
10
|
def initialize(context, response)
|
|
13
11
|
@context = context
|
|
@@ -18,9 +16,7 @@ module Restify
|
|
|
18
16
|
@resource ||= begin
|
|
19
17
|
resource = load
|
|
20
18
|
|
|
21
|
-
unless resource.is_a? Restify::Resource
|
|
22
|
-
resource = Resource.new context, response: response, data: resource
|
|
23
|
-
end
|
|
19
|
+
resource = Resource.new context, response: response, data: resource unless resource.is_a? Restify::Resource
|
|
24
20
|
|
|
25
21
|
resource._restify_response = response
|
|
26
22
|
merge_relations! resource._restify_relations
|
|
@@ -24,9 +24,7 @@ module Restify
|
|
|
24
24
|
data = object.each_with_object({}, &method(:parse_data))
|
|
25
25
|
relations = object.each_with_object({}, &method(:parse_rels))
|
|
26
26
|
|
|
27
|
-
if self.class.indifferent_access?
|
|
28
|
-
data = with_indifferent_access(data)
|
|
29
|
-
end
|
|
27
|
+
data = with_indifferent_access(data) if self.class.indifferent_access?
|
|
30
28
|
|
|
31
29
|
Resource.new context,
|
|
32
30
|
data: data,
|
|
@@ -56,9 +54,7 @@ module Restify
|
|
|
56
54
|
return
|
|
57
55
|
end
|
|
58
56
|
|
|
59
|
-
if relations.key?(name) || pair[1].nil? || pair[1].to_s =~ /\A\w*\z/
|
|
60
|
-
return
|
|
61
|
-
end
|
|
57
|
+
return if relations.key?(name) || pair[1].nil? || pair[1].to_s =~ /\A\w*\z/
|
|
62
58
|
|
|
63
59
|
relations[name] = pair[1].to_s
|
|
64
60
|
end
|
data/lib/restify/promise.rb
CHANGED
|
@@ -11,9 +11,7 @@ module Restify
|
|
|
11
11
|
# When dependencies were passed in, but none are left after flattening,
|
|
12
12
|
# then we don't have to wait for explicit dependencies or resolution
|
|
13
13
|
# through a writer.
|
|
14
|
-
if !@task && @dependencies.empty? && dependencies.any?
|
|
15
|
-
complete true, [], nil
|
|
16
|
-
end
|
|
14
|
+
complete true, [], nil if !@task && @dependencies.empty? && dependencies.any?
|
|
17
15
|
end
|
|
18
16
|
|
|
19
17
|
def wait(timeout = nil)
|
data/lib/restify/resource.rb
CHANGED
data/lib/restify/timeout.rb
CHANGED
|
@@ -51,9 +51,7 @@ module Restify
|
|
|
51
51
|
"Timeout must be an number but is #{value}"
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
raise ArgumentError.new "Timeout must be > 0 but is #{value.inspect}."
|
|
56
|
-
end
|
|
54
|
+
raise ArgumentError.new "Timeout must be > 0 but is #{value.inspect}." unless value.positive?
|
|
57
55
|
|
|
58
56
|
value
|
|
59
57
|
end
|
data/lib/restify/version.rb
CHANGED
|
@@ -4,20 +4,19 @@ require 'spec_helper'
|
|
|
4
4
|
|
|
5
5
|
describe Restify do
|
|
6
6
|
let!(:request_stub) do
|
|
7
|
-
stub_request(:head, 'http://
|
|
7
|
+
stub_request(:head, 'http://stubserver/base')
|
|
8
8
|
.with(query: hash_including({}))
|
|
9
9
|
.to_return do
|
|
10
|
-
|
|
10
|
+
<<~HTTP
|
|
11
11
|
HTTP/1.1 200 OK
|
|
12
12
|
Content-Length: 333
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
RESPONSE
|
|
13
|
+
Link: <http://localhost:9292/other>; rel="neat"
|
|
14
|
+
HTTP
|
|
16
15
|
end
|
|
17
16
|
end
|
|
18
17
|
|
|
19
18
|
describe 'HEAD requests' do
|
|
20
|
-
subject { Restify.new('http://localhost/base').head(params).value! }
|
|
19
|
+
subject { Restify.new('http://localhost:9292/base').head(params).value! }
|
|
21
20
|
let(:params) { {} }
|
|
22
21
|
|
|
23
22
|
it 'returns a resource with access to headers' do
|
|
@@ -4,19 +4,16 @@ require 'spec_helper'
|
|
|
4
4
|
|
|
5
5
|
describe Restify do
|
|
6
6
|
let!(:request_stub) do
|
|
7
|
-
stub_request(:post, 'http://
|
|
8
|
-
|
|
9
|
-
<<-RESPONSE.gsub(/^ {8}/, '')
|
|
7
|
+
stub_request(:post, 'http://stubserver/base').to_return do
|
|
8
|
+
<<~HTTP
|
|
10
9
|
HTTP/1.1 200 OK
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
Link: <http://localhost/other>; rel="neat"
|
|
14
|
-
RESPONSE
|
|
10
|
+
Link: <http://localhost:9292/other>; rel="neat"
|
|
11
|
+
HTTP
|
|
15
12
|
end
|
|
16
13
|
end
|
|
17
14
|
|
|
18
15
|
describe 'Request body' do
|
|
19
|
-
subject { Restify.new('http://localhost/base').post(body, {}, {headers: headers}).value! }
|
|
16
|
+
subject { Restify.new('http://localhost:9292/base').post(body, {}, {headers: headers}).value! }
|
|
20
17
|
let(:headers) { {} }
|
|
21
18
|
|
|
22
19
|
context 'with JSON-like data structures' do
|
|
@@ -66,18 +63,18 @@ describe Restify do
|
|
|
66
63
|
subject
|
|
67
64
|
|
|
68
65
|
expect(
|
|
69
|
-
request_stub.with {|req| req.headers['Content-Type']
|
|
66
|
+
request_stub.with {|req| req.headers['Content-Type'] !~ /json/ }
|
|
70
67
|
).to have_been_requested
|
|
71
68
|
end
|
|
72
69
|
|
|
73
70
|
context 'with overridden media type' do
|
|
74
|
-
let(:headers) { {'Content-Type' => 'application/
|
|
71
|
+
let(:headers) { {'Content-Type' => 'application/text'} }
|
|
75
72
|
|
|
76
73
|
it 'respects the override' do
|
|
77
74
|
subject
|
|
78
75
|
|
|
79
76
|
expect(
|
|
80
|
-
request_stub.with(headers: {'Content-Type' => 'application/
|
|
77
|
+
request_stub.with(headers: {'Content-Type' => 'application/text'})
|
|
81
78
|
).to have_been_requested
|
|
82
79
|
end
|
|
83
80
|
end
|
|
@@ -4,20 +4,19 @@ require 'spec_helper'
|
|
|
4
4
|
|
|
5
5
|
describe Restify do
|
|
6
6
|
let!(:request_stub) do
|
|
7
|
-
stub_request(:get,
|
|
8
|
-
|
|
7
|
+
stub_request(:get, "http://stubserver/base").to_return do
|
|
8
|
+
<<~HTTP
|
|
9
9
|
HTTP/1.1 200 OK
|
|
10
10
|
Content-Type: application/json
|
|
11
|
-
|
|
12
|
-
Link: <http://localhost/base>; rel="self"
|
|
11
|
+
Link: <http://localhost:9292/base>; rel="self"
|
|
13
12
|
|
|
14
13
|
{ "response": "success" }
|
|
15
|
-
|
|
14
|
+
HTTP
|
|
16
15
|
end
|
|
17
16
|
end
|
|
18
17
|
|
|
19
18
|
context 'with request headers configured for a single request' do
|
|
20
|
-
let(:context) { Restify.new('http://localhost/base') }
|
|
19
|
+
let(:context) { Restify.new('http://localhost:9292/base') }
|
|
21
20
|
|
|
22
21
|
it 'sends the headers only for that request' do
|
|
23
22
|
root = context.get(
|
|
@@ -37,7 +36,7 @@ describe Restify do
|
|
|
37
36
|
context 'with request headers configured for context' do
|
|
38
37
|
let(:context) do
|
|
39
38
|
Restify.new(
|
|
40
|
-
'http://localhost/base',
|
|
39
|
+
'http://localhost:9292/base',
|
|
41
40
|
headers: {'Accept' => 'application/msgpack, application/json'}
|
|
42
41
|
)
|
|
43
42
|
end
|
|
@@ -4,21 +4,15 @@ require 'spec_helper'
|
|
|
4
4
|
|
|
5
5
|
describe Restify do
|
|
6
6
|
let!(:request_stub) do
|
|
7
|
-
stub_request(:get, 'http://
|
|
8
|
-
.to_return
|
|
9
|
-
<<-RESPONSE.gsub(/^ {8}/, '')
|
|
10
|
-
HTTP/1.1 #{http_status}
|
|
11
|
-
Content-Length: 333
|
|
12
|
-
Transfer-Encoding: chunked
|
|
13
|
-
Link: <http://localhost/other>; rel="neat"
|
|
14
|
-
RESPONSE
|
|
15
|
-
end
|
|
7
|
+
stub_request(:get, 'http://stubserver/base')
|
|
8
|
+
.to_return(status: http_status, headers: headers)
|
|
16
9
|
end
|
|
17
10
|
|
|
18
11
|
let(:http_status) { '200 OK' }
|
|
12
|
+
let(:headers) { {} }
|
|
19
13
|
|
|
20
14
|
describe 'Error handling' do
|
|
21
|
-
subject(:request) { Restify.new('http://localhost/base').get.value! }
|
|
15
|
+
subject(:request) { Restify.new('http://localhost:9292/base').get.value! }
|
|
22
16
|
|
|
23
17
|
context 'for 400 status codes' do
|
|
24
18
|
let(:http_status) { '400 Bad Request' }
|
|
@@ -60,6 +54,60 @@ describe Restify do
|
|
|
60
54
|
end
|
|
61
55
|
end
|
|
62
56
|
|
|
57
|
+
context 'for 429 status codes' do
|
|
58
|
+
let(:http_status) { '429 Too Many Requests' }
|
|
59
|
+
|
|
60
|
+
it 'throws a TooManyRequests exception' do
|
|
61
|
+
expect { request }.to raise_error Restify::TooManyRequests
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
describe 'the exception' do
|
|
65
|
+
subject(:exception) do
|
|
66
|
+
exception = nil
|
|
67
|
+
begin
|
|
68
|
+
request
|
|
69
|
+
rescue Restify::TooManyRequests => e
|
|
70
|
+
exception = e
|
|
71
|
+
end
|
|
72
|
+
exception
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
context 'by default' do
|
|
76
|
+
it 'does not know when to retry again' do
|
|
77
|
+
expect(exception.retry_after).to be_nil
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
context 'with Retry-After header containing seconds' do
|
|
82
|
+
let(:headers) { {'Retry-After' => '120'} }
|
|
83
|
+
|
|
84
|
+
it 'determines the date correctly' do
|
|
85
|
+
now = DateTime.now
|
|
86
|
+
lower = now + Rational(119, 86_400)
|
|
87
|
+
upper = now + Rational(121, 86_400)
|
|
88
|
+
|
|
89
|
+
expect(exception.retry_after).to be_between(lower, upper)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
context 'with Retry-After header containing HTTP date' do
|
|
94
|
+
let(:headers) { {'Retry-After' => 'Sun, 13 Mar 2033 13:03:33 GMT'} }
|
|
95
|
+
|
|
96
|
+
it 'parses the date correctly' do
|
|
97
|
+
expect(exception.retry_after.to_s).to eq '2033-03-13T13:03:33+00:00'
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
context 'with Retry-After header containing invalid date string' do
|
|
102
|
+
let(:headers) { {'Retry-After' => 'tomorrow 12:00:00'} }
|
|
103
|
+
|
|
104
|
+
it 'does not know when to retry again' do
|
|
105
|
+
expect(exception.retry_after).to be_nil
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
63
111
|
context 'for any other 4xx status codes' do
|
|
64
112
|
let(:http_status) { '415 Unsupported Media Type' }
|
|
65
113
|
|
data/spec/restify/global_spec.rb
CHANGED
|
@@ -29,7 +29,7 @@ describe Restify::Global do
|
|
|
29
29
|
subject { global.new(name, **options) }
|
|
30
30
|
|
|
31
31
|
it 'returns relation for stored registry item' do
|
|
32
|
-
Restify::Registry.store
|
|
32
|
+
Restify::Registry.store(name, uri, **options)
|
|
33
33
|
|
|
34
34
|
expect(subject).to be_a Restify::Relation
|
|
35
35
|
expect(subject.pattern).to eq uri
|
data/spec/restify_spec.rb
CHANGED
|
@@ -5,99 +5,94 @@ require 'spec_helper'
|
|
|
5
5
|
describe Restify do
|
|
6
6
|
context 'as a dynamic HATEOAS client' do
|
|
7
7
|
before do
|
|
8
|
-
stub_request(:get, 'http://
|
|
9
|
-
|
|
8
|
+
stub_request(:get, 'http://stubserver/base').to_return do
|
|
9
|
+
<<~HTTP
|
|
10
10
|
HTTP/1.1 200 OK
|
|
11
11
|
Content-Type: application/json
|
|
12
|
-
|
|
13
|
-
Link: <http://localhost/base/
|
|
14
|
-
Link: <http://localhost/base/courses{/id}>; rel="courses"
|
|
12
|
+
Link: <http://localhost:9292/base/users{/id}>; rel="users"
|
|
13
|
+
Link: <http://localhost:9292/base/courses{/id}>; rel="courses"
|
|
15
14
|
|
|
16
15
|
{
|
|
17
|
-
"profile_url": "http://localhost/base/profile",
|
|
18
|
-
"search_url": "http://localhost/base/search?q={query}",
|
|
16
|
+
"profile_url": "http://localhost:9292/base/profile",
|
|
17
|
+
"search_url": "http://localhost:9292/base/search?q={query}",
|
|
19
18
|
"mirror_url": null
|
|
20
19
|
}
|
|
21
|
-
|
|
20
|
+
HTTP
|
|
22
21
|
end
|
|
23
22
|
|
|
24
|
-
stub_request(:get, 'http://
|
|
25
|
-
|
|
23
|
+
stub_request(:get, 'http://stubserver/base/users')
|
|
24
|
+
.to_return do
|
|
25
|
+
<<~HTTP
|
|
26
26
|
HTTP/1.1 200 OK
|
|
27
27
|
Content-Type: application/json
|
|
28
|
-
Transfer-Encoding: chunked
|
|
29
28
|
|
|
30
29
|
[{
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
30
|
+
"name": "John Smith",
|
|
31
|
+
"url": "http://localhost:9292/base/users/john.smith",
|
|
32
|
+
"blurb_url": "http://localhost:9292/base/users/john.smith/blurb",
|
|
33
|
+
"languages": ["de", "en"]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"name": "Jane Smith",
|
|
37
|
+
"self_url": "http://localhost:9292/base/user/jane.smith"
|
|
38
|
+
}]
|
|
39
|
+
HTTP
|
|
41
40
|
end
|
|
42
41
|
|
|
43
|
-
stub_request(:post, 'http://
|
|
42
|
+
stub_request(:post, 'http://stubserver/base/users')
|
|
44
43
|
.with(body: {})
|
|
45
44
|
.to_return do
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
Transfer-Encoding: chunked
|
|
45
|
+
<<~HTTP
|
|
46
|
+
HTTP/1.1 422 Unprocessable Entity
|
|
47
|
+
Content-Type: application/json
|
|
50
48
|
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
{"errors":{"name":["can't be blank"]}}
|
|
50
|
+
HTTP
|
|
53
51
|
end
|
|
54
52
|
|
|
55
|
-
stub_request(:post, 'http://
|
|
53
|
+
stub_request(:post, 'http://stubserver/base/users')
|
|
56
54
|
.with(body: {name: 'John Smith'})
|
|
57
55
|
.to_return do
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
RESPONSE
|
|
56
|
+
<<~HTTP
|
|
57
|
+
HTTP/1.1 201 Created
|
|
58
|
+
Content-Type: application/json
|
|
59
|
+
Location: http://localhost:9292/base/users/john.smith
|
|
60
|
+
|
|
61
|
+
{
|
|
62
|
+
"name": "John Smith",
|
|
63
|
+
"url": "http://localhost:9292/base/users/john.smith",
|
|
64
|
+
"blurb_url": "http://localhost:9292/base/users/john.smith/blurb",
|
|
65
|
+
"languages": ["de", "en"]
|
|
66
|
+
}
|
|
67
|
+
HTTP
|
|
71
68
|
end
|
|
72
69
|
|
|
73
|
-
stub_request(:get, 'http://
|
|
70
|
+
stub_request(:get, 'http://stubserver/base/users/john.smith')
|
|
74
71
|
.to_return do
|
|
75
|
-
|
|
72
|
+
<<~HTTP
|
|
76
73
|
HTTP/1.1 200 OK
|
|
77
74
|
Content-Type: application/json
|
|
78
|
-
Link: <http://localhost/base/users/john.smith>; rel="self"
|
|
79
|
-
Transfer-Encoding: chunked
|
|
75
|
+
Link: <http://localhost:9292/base/users/john.smith>; rel="self"
|
|
80
76
|
|
|
81
77
|
{
|
|
82
78
|
"name": "John Smith",
|
|
83
|
-
"url": "http://localhost/base/users/john.smith"
|
|
79
|
+
"url": "http://localhost:9292/base/users/john.smith"
|
|
84
80
|
}
|
|
85
|
-
|
|
81
|
+
HTTP
|
|
86
82
|
end
|
|
87
83
|
|
|
88
|
-
stub_request(:get, 'http://
|
|
84
|
+
stub_request(:get, 'http://stubserver/base/users/john.smith/blurb')
|
|
89
85
|
.to_return do
|
|
90
|
-
|
|
86
|
+
<<~HTTP
|
|
91
87
|
HTTP/1.1 200 OK
|
|
92
88
|
Content-Type: application/json
|
|
93
|
-
Link: <http://localhost/base/users/john.smith>; rel="user"
|
|
94
|
-
Transfer-Encoding: chunked
|
|
89
|
+
Link: <http://localhost:9292/base/users/john.smith>; rel="user"
|
|
95
90
|
|
|
96
91
|
{
|
|
97
92
|
"title": "Prof. Dr. John Smith",
|
|
98
93
|
"image": "http://example.org/avatar.png"
|
|
99
94
|
}
|
|
100
|
-
|
|
95
|
+
HTTP
|
|
101
96
|
end
|
|
102
97
|
end
|
|
103
98
|
|
|
@@ -107,7 +102,7 @@ describe Restify do
|
|
|
107
102
|
|
|
108
103
|
# First request the entry resource usually the
|
|
109
104
|
# root using GET and wait for it.
|
|
110
|
-
root = Restify.new('http://localhost/base').get.value!
|
|
105
|
+
root = Restify.new('http://localhost:9292/base').get.value!
|
|
111
106
|
|
|
112
107
|
# Therefore we need the `users` relations of our root
|
|
113
108
|
# resource.
|
|
@@ -194,7 +189,7 @@ describe Restify do
|
|
|
194
189
|
skip 'Seems to be impossible to detect EM scheduled fibers from within'
|
|
195
190
|
|
|
196
191
|
EM.synchrony do
|
|
197
|
-
root = Restify.new('http://localhost/base').get.value!
|
|
192
|
+
root = Restify.new('http://localhost:9292/base').get.value!
|
|
198
193
|
|
|
199
194
|
users_relation = root.rel(:users)
|
|
200
195
|
|
data/spec/spec_helper.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'rspec'
|
|
4
|
-
require '
|
|
4
|
+
require 'rspec/collection_matchers'
|
|
5
5
|
|
|
6
6
|
require 'simplecov'
|
|
7
7
|
SimpleCov.start do
|
|
@@ -34,17 +34,13 @@ if ENV['ADAPTER']
|
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
require 'rspec/collection_matchers'
|
|
39
|
-
require 'em-synchrony'
|
|
40
|
-
|
|
41
|
-
Dir[File.expand_path('spec/support/**/*.rb')].sort.each {|f| require f }
|
|
37
|
+
require_relative 'support/stub_server.rb'
|
|
42
38
|
|
|
43
39
|
RSpec.configure do |config|
|
|
44
40
|
config.order = 'random'
|
|
45
41
|
|
|
46
42
|
config.before(:suite) do
|
|
47
|
-
::Restify::Timeout.default_timeout =
|
|
43
|
+
::Restify::Timeout.default_timeout = 0.1
|
|
48
44
|
end
|
|
49
45
|
|
|
50
46
|
config.before(:each) do
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'puma'
|
|
4
|
+
require 'rack'
|
|
5
|
+
require 'webmock'
|
|
6
|
+
require 'webmock/rspec/matchers'
|
|
7
|
+
|
|
8
|
+
module Stub
|
|
9
|
+
# This Rack application matches the request received from rack against the
|
|
10
|
+
# webmock stub database and returns the response.
|
|
11
|
+
#
|
|
12
|
+
# A custom server name is used to
|
|
13
|
+
# 1) has a stable name without a dynamic port for easier `#stub_request`
|
|
14
|
+
# calls, and
|
|
15
|
+
# 2) to ensure no actual request is intercepted (they are send to
|
|
16
|
+
# `localhost:<port>`).
|
|
17
|
+
#
|
|
18
|
+
# If no stub is found a special HTTP 599 error code will be returned.
|
|
19
|
+
class Handler
|
|
20
|
+
def call(env) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
21
|
+
signature = WebMock::RequestSignature.new(
|
|
22
|
+
env['REQUEST_METHOD'].downcase,
|
|
23
|
+
"http://stubserver#{env['REQUEST_URI']}"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Extract request headers from rack env. Most header should start with
|
|
27
|
+
# `HTTP_` but at least content type is present as `CONTENT_TYPE`.
|
|
28
|
+
headers = {}
|
|
29
|
+
env.each_pair do |key, value|
|
|
30
|
+
case key
|
|
31
|
+
when /^HTTP_(.*)$/, /^(CONTENT_.*)$/
|
|
32
|
+
headers[Regexp.last_match(1)] = value
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Read request body from socket into string
|
|
37
|
+
signature.body = env['rack.input'].read
|
|
38
|
+
signature.headers = headers
|
|
39
|
+
|
|
40
|
+
WebMock::RequestRegistry.instance.requested_signatures.put(signature)
|
|
41
|
+
response = ::WebMock::StubRegistry.instance.response_for_request(signature)
|
|
42
|
+
|
|
43
|
+
# If no stub matched `nil` is returned.
|
|
44
|
+
if response
|
|
45
|
+
status = response.status
|
|
46
|
+
status = status.to_s.split(' ', 2) unless status.is_a?(Array)
|
|
47
|
+
status = Integer(status[0])
|
|
48
|
+
|
|
49
|
+
[status, response.headers || {}, [response.body.to_s]]
|
|
50
|
+
else
|
|
51
|
+
# Return special HTTP 599 with the error message that would normally
|
|
52
|
+
# appear on missing stubs.
|
|
53
|
+
[599, {}, [WebMock::NetConnectNotAllowedError.new(signature).message]]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class Exception < ::StandardError; end
|
|
59
|
+
|
|
60
|
+
# Inject into base adapter to have HTTP 599 (missing stub) error raised as an
|
|
61
|
+
# extra exception, not just a server error.
|
|
62
|
+
module Patch
|
|
63
|
+
def call(request)
|
|
64
|
+
super.then do |response|
|
|
65
|
+
next response unless response.code == 599
|
|
66
|
+
|
|
67
|
+
raise ::Stub::Exception.new(response.body)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
::Restify::Adapter::Base.prepend(self)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class << self
|
|
75
|
+
def start_server!
|
|
76
|
+
@server = ::Puma::Server.new(Handler.new)
|
|
77
|
+
@server.add_tcp_listener('localhost', 9292)
|
|
78
|
+
|
|
79
|
+
Thread.new do
|
|
80
|
+
@server.run
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
RSpec.configure do |config|
|
|
87
|
+
config.include WebMock::API
|
|
88
|
+
config.include WebMock::Matchers
|
|
89
|
+
|
|
90
|
+
config.before(:suite) do
|
|
91
|
+
Stub.start_server!
|
|
92
|
+
|
|
93
|
+
# Net::HTTP adapter must be enabled, otherwise webmock fails to create mock
|
|
94
|
+
# responses from raw strings.
|
|
95
|
+
WebMock.disable!(except: %i[net_http])
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
config.around(:each) do |example|
|
|
99
|
+
example.run
|
|
100
|
+
WebMock.reset!
|
|
101
|
+
end
|
|
102
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: restify
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.15.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jan Graichen
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2021-07-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -209,6 +209,7 @@ files:
|
|
|
209
209
|
- spec/restify/timeout_spec.rb
|
|
210
210
|
- spec/restify_spec.rb
|
|
211
211
|
- spec/spec_helper.rb
|
|
212
|
+
- spec/support/stub_server.rb
|
|
212
213
|
homepage: https://github.com/jgraichen/restify
|
|
213
214
|
licenses:
|
|
214
215
|
- LGPL-3.0+
|
|
@@ -221,14 +222,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
221
222
|
requirements:
|
|
222
223
|
- - ">="
|
|
223
224
|
- !ruby/object:Gem::Version
|
|
224
|
-
version:
|
|
225
|
+
version: 2.5.0
|
|
225
226
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
226
227
|
requirements:
|
|
227
228
|
- - ">="
|
|
228
229
|
- !ruby/object:Gem::Version
|
|
229
230
|
version: '0'
|
|
230
231
|
requirements: []
|
|
231
|
-
rubygems_version: 3.
|
|
232
|
+
rubygems_version: 3.2.22
|
|
232
233
|
signing_key:
|
|
233
234
|
specification_version: 4
|
|
234
235
|
summary: An experimental hypermedia REST client.
|
|
@@ -252,3 +253,4 @@ test_files:
|
|
|
252
253
|
- spec/restify/timeout_spec.rb
|
|
253
254
|
- spec/restify_spec.rb
|
|
254
255
|
- spec/spec_helper.rb
|
|
256
|
+
- spec/support/stub_server.rb
|