restify 1.12.0 → 1.15.1
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 +36 -0
- data/README.md +9 -12
- data/lib/restify/adapter/em.rb +6 -8
- data/lib/restify/adapter/pooled_em.rb +36 -40
- data/lib/restify/adapter/typhoeus.rb +74 -52
- data/lib/restify/context.rb +4 -4
- data/lib/restify/error.rb +24 -0
- data/lib/restify/global.rb +2 -2
- 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/request.rb +13 -5
- data/lib/restify/resource.rb +1 -1
- data/lib/restify/timeout.rb +1 -3
- data/lib/restify/version.rb +2 -2
- data/spec/restify/context_spec.rb +2 -2
- data/spec/restify/error_spec.rb +0 -1
- data/spec/restify/features/head_requests_spec.rb +5 -6
- data/spec/restify/features/request_bodies_spec.rb +83 -0
- data/spec/restify/features/request_errors_spec.rb +19 -0
- data/spec/restify/features/request_headers_spec.rb +6 -7
- data/spec/restify/features/response_errors_spec.rb +127 -0
- data/spec/restify/global_spec.rb +2 -2
- data/spec/restify_spec.rb +50 -57
- data/spec/spec_helper.rb +10 -7
- data/spec/support/stub_server.rb +106 -0
- metadata +12 -6
- data/spec/restify/features/response_errors.rb +0 -79
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
|
data/lib/restify/global.rb
CHANGED
@@ -39,9 +39,9 @@ module Restify
|
|
39
39
|
|
40
40
|
def resolve_context(uri, **opts)
|
41
41
|
if uri.is_a? Symbol
|
42
|
-
Restify::Registry.fetch(uri).inherit(nil, opts)
|
42
|
+
Restify::Registry.fetch(uri).inherit(nil, **opts)
|
43
43
|
else
|
44
|
-
Context.new
|
44
|
+
Context.new(uri, **opts)
|
45
45
|
end
|
46
46
|
end
|
47
47
|
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/request.rb
CHANGED
@@ -34,18 +34,26 @@ module Restify
|
|
34
34
|
@uri = opts.fetch(:uri) { raise ArgumentError.new ':uri required.' }
|
35
35
|
@data = opts.fetch(:data, nil)
|
36
36
|
@timeout = opts.fetch(:timeout, 300)
|
37
|
-
@headers = opts.fetch(:headers, {})
|
38
|
-
|
37
|
+
@headers = opts.fetch(:headers, {})
|
38
|
+
|
39
|
+
@headers['Content-Type'] ||= 'application/json' if json?
|
39
40
|
end
|
40
41
|
|
41
42
|
def body
|
42
|
-
@body ||=
|
43
|
-
JSON.dump(data) unless data.nil?
|
44
|
-
end
|
43
|
+
@body ||= json? ? JSON.dump(@data) : @data
|
45
44
|
end
|
46
45
|
|
47
46
|
def to_s
|
48
47
|
"#<#{self.class} #{method.upcase} #{uri}>"
|
49
48
|
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def json?
|
53
|
+
return false if @data.nil?
|
54
|
+
return false if @data.is_a? String
|
55
|
+
|
56
|
+
true
|
57
|
+
end
|
50
58
|
end
|
51
59
|
end
|
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
@@ -47,7 +47,7 @@ describe Restify::Context do
|
|
47
47
|
|
48
48
|
context 'YAML' do
|
49
49
|
let(:dump) { YAML.dump(context) }
|
50
|
-
let(:load) { YAML.load(dump) } # rubocop:disable YAMLLoad
|
50
|
+
let(:load) { YAML.load(dump) } # rubocop:disable Security/YAMLLoad
|
51
51
|
|
52
52
|
subject { load }
|
53
53
|
|
@@ -56,7 +56,7 @@ describe Restify::Context do
|
|
56
56
|
|
57
57
|
context 'Marshall' do
|
58
58
|
let(:dump) { Marshal.dump(context) }
|
59
|
-
let(:load) { Marshal.load(dump) } # rubocop:disable MarshalLoad
|
59
|
+
let(:load) { Marshal.load(dump) } # rubocop:disable Security/MarshalLoad
|
60
60
|
|
61
61
|
subject { load }
|
62
62
|
|
data/spec/restify/error_spec.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
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Restify do
|
6
|
+
let!(:request_stub) do
|
7
|
+
stub_request(:post, 'http://stubserver/base').to_return do
|
8
|
+
<<~HTTP
|
9
|
+
HTTP/1.1 200 OK
|
10
|
+
Link: <http://localhost:9292/other>; rel="neat"
|
11
|
+
HTTP
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe 'Request body' do
|
16
|
+
subject { Restify.new('http://localhost:9292/base').post(body, {}, {headers: headers}).value! }
|
17
|
+
let(:headers) { {} }
|
18
|
+
|
19
|
+
context 'with JSON-like data structures' do
|
20
|
+
let(:body) { {a: 'b', c: 'd'} }
|
21
|
+
|
22
|
+
it 'is serialized as JSON' do
|
23
|
+
subject
|
24
|
+
|
25
|
+
expect(
|
26
|
+
request_stub.with(body: '{"a":"b","c":"d"}')
|
27
|
+
).to have_been_requested
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'gets a JSON media type for free' do
|
31
|
+
subject
|
32
|
+
|
33
|
+
expect(
|
34
|
+
request_stub.with(headers: {'Content-Type' => 'application/json'})
|
35
|
+
).to have_been_requested
|
36
|
+
end
|
37
|
+
|
38
|
+
context 'with overridden media type' do
|
39
|
+
let(:headers) { {'Content-Type' => 'application/vnd.api+json'} }
|
40
|
+
|
41
|
+
it 'respects the override' do
|
42
|
+
subject
|
43
|
+
|
44
|
+
expect(
|
45
|
+
request_stub.with(headers: {'Content-Type' => 'application/vnd.api+json'})
|
46
|
+
).to have_been_requested
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context 'with strings' do
|
52
|
+
let(:body) { 'a=b&c=d' }
|
53
|
+
|
54
|
+
it 'is sent as provided' do
|
55
|
+
subject
|
56
|
+
|
57
|
+
expect(
|
58
|
+
request_stub.with(body: 'a=b&c=d')
|
59
|
+
).to have_been_requested
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'does not get a JSON media type' do
|
63
|
+
subject
|
64
|
+
|
65
|
+
expect(
|
66
|
+
request_stub.with {|req| req.headers['Content-Type'] !~ /json/ }
|
67
|
+
).to have_been_requested
|
68
|
+
end
|
69
|
+
|
70
|
+
context 'with overridden media type' do
|
71
|
+
let(:headers) { {'Content-Type' => 'application/text'} }
|
72
|
+
|
73
|
+
it 'respects the override' do
|
74
|
+
subject
|
75
|
+
|
76
|
+
expect(
|
77
|
+
request_stub.with(headers: {'Content-Type' => 'application/text'})
|
78
|
+
).to have_been_requested
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Restify, adapter: ::Restify::Adapter::Typhoeus do
|
6
|
+
before do
|
7
|
+
stub_request(:get, 'http://stubserver/base').to_timeout
|
8
|
+
end
|
9
|
+
|
10
|
+
describe 'Timeout' do
|
11
|
+
subject(:request) { Restify.new('http://localhost:9292/base').get({}, timeout: 0.1).value! }
|
12
|
+
|
13
|
+
it 'throws a network error' do
|
14
|
+
expect { request }.to raise_error Restify::NetworkError do |error|
|
15
|
+
expect(error.message).to match(/timeout/i)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
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
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Restify do
|
6
|
+
let!(:request_stub) do
|
7
|
+
stub_request(:get, 'http://stubserver/base')
|
8
|
+
.to_return(status: http_status, headers: headers)
|
9
|
+
end
|
10
|
+
|
11
|
+
let(:http_status) { '200 OK' }
|
12
|
+
let(:headers) { {} }
|
13
|
+
|
14
|
+
describe 'Error handling' do
|
15
|
+
subject(:request) { Restify.new('http://localhost:9292/base').get.value! }
|
16
|
+
|
17
|
+
context 'for 400 status codes' do
|
18
|
+
let(:http_status) { '400 Bad Request' }
|
19
|
+
|
20
|
+
it 'throws a BadRequest exception' do
|
21
|
+
expect { request }.to raise_error Restify::BadRequest
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'for 401 status codes' do
|
26
|
+
let(:http_status) { '401 Unauthorized' }
|
27
|
+
|
28
|
+
it 'throws an Unauthorized exception' do
|
29
|
+
expect { request }.to raise_error Restify::Unauthorized
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'for 404 status codes' do
|
34
|
+
let(:http_status) { '404 Not Found' }
|
35
|
+
|
36
|
+
it 'throws a ClientError exception' do
|
37
|
+
expect { request }.to raise_error Restify::NotFound
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'for 406 status codes' do
|
42
|
+
let(:http_status) { '406 Not Acceptable' }
|
43
|
+
|
44
|
+
it 'throws a NotAcceptable exception' do
|
45
|
+
expect { request }.to raise_error Restify::NotAcceptable
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'for 422 status codes' do
|
50
|
+
let(:http_status) { '422 Unprocessable Entity' }
|
51
|
+
|
52
|
+
it 'throws a UnprocessableEntity exception' do
|
53
|
+
expect { request }.to raise_error Restify::UnprocessableEntity
|
54
|
+
end
|
55
|
+
end
|
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
|
+
|
111
|
+
context 'for any other 4xx status codes' do
|
112
|
+
let(:http_status) { '415 Unsupported Media Type' }
|
113
|
+
|
114
|
+
it 'throws a generic ClientError exception' do
|
115
|
+
expect { request }.to raise_error Restify::ClientError
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
context 'for any 5xx status codes' do
|
120
|
+
let(:http_status) { '500 Internal Server Error' }
|
121
|
+
|
122
|
+
it 'throws a generic ServerError exception' do
|
123
|
+
expect { request }.to raise_error Restify::ServerError
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|