restify 1.12.0 → 1.15.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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 uri, opts
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
@@ -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)
@@ -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, {}).merge \
38
- 'Content-Type' => 'application/json'
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 ||= begin
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
@@ -97,7 +97,7 @@ module Restify
97
97
  text = {
98
98
  '@data' => data,
99
99
  '@relations' => @relations
100
- }.map {|k, v| k + '=' + v.inspect }.join(' ')
100
+ }.map {|k, v| "#{k}=#{v.inspect}" }.join(' ')
101
101
 
102
102
  "#<#{self.class} #{text}>"
103
103
  end
@@ -51,9 +51,7 @@ module Restify
51
51
  "Timeout must be an number but is #{value}"
52
52
  end
53
53
 
54
- unless value > 0
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
@@ -3,8 +3,8 @@
3
3
  module Restify
4
4
  module VERSION
5
5
  MAJOR = 1
6
- MINOR = 12
7
- PATCH = 0
6
+ MINOR = 15
7
+ PATCH = 1
8
8
  STAGE = nil
9
9
  STRING = [MAJOR, MINOR, PATCH, STAGE].reject(&:nil?).join('.').freeze
10
10
 
@@ -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
 
@@ -14,7 +14,6 @@ describe Restify::ResponseError do
14
14
  allow(response).to receive(:decoded_body).and_return({})
15
15
  end
16
16
 
17
-
18
17
  describe '.from_code' do
19
18
  subject(:err) { described_class.from_code(response) }
20
19
 
@@ -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://localhost/base')
7
+ stub_request(:head, 'http://stubserver/base')
8
8
  .with(query: hash_including({}))
9
9
  .to_return do
10
- <<-RESPONSE.gsub(/^ {8}/, '')
10
+ <<~HTTP
11
11
  HTTP/1.1 200 OK
12
12
  Content-Length: 333
13
- Transfer-Encoding: chunked
14
- Link: <http://localhost/other>; rel="neat"
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, 'http://localhost/base').to_return do
8
- <<-RESPONSE.gsub(/^ {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
- Transfer-Encoding: chunked
12
- Link: <http://localhost/base>; rel="self"
11
+ Link: <http://localhost:9292/base>; rel="self"
13
12
 
14
13
  { "response": "success" }
15
- RESPONSE
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