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.
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