flexirest 1.12.4 → 1.13.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/.github/workflows/build.yml +1 -1
- data/CHANGELOG.md +12 -0
- data/docs/basic-usage.md +6 -0
- data/lib/flexirest/associations.rb +8 -4
- data/lib/flexirest/connection.rb +12 -0
- data/lib/flexirest/instrumentation.rb +9 -6
- data/lib/flexirest/json_api_proxy.rb +20 -4
- data/lib/flexirest/logger.rb +32 -38
- data/lib/flexirest/proxy_base.rb +66 -39
- data/lib/flexirest/request.rb +10 -9
- data/lib/flexirest/version.rb +1 -1
- data/spec/lib/associations_spec.rb +31 -0
- data/spec/lib/json_api_spec.rb +61 -0
- data/spec/lib/proxy_spec.rb +65 -0
- data/spec/lib/request_spec.rb +6 -0
- data/spec/spec_helper.rb +0 -2
- metadata +3 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4953df31ca4ca1d055e7a80ce2cd7a0df8e53cc4b67544b80d1a91ee2aa366a4
|
|
4
|
+
data.tar.gz: 0f500061fd78240f0f2c2d8c19934d5c6c5f777f32de82c8bc2330415fd6c768
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6c45c501ef89cc34a66a66ca585088e7af146348b2c42ffd0c36fe4559a957ae4bc5f579b207b238c5d0e5a0625bb1000f937a144a2107ce3eff79272d07286a
|
|
7
|
+
data.tar.gz: 9ea32ee11733ee70ea539d37f094fb8e9f730c82c4a14f84e5322102ab380af0c5dc12a3ac258968e8ec9a6ebd6b84ae780e54ec9a518744888c6ee62be40e7f
|
data/.github/workflows/build.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.13.0
|
|
4
|
+
|
|
5
|
+
Bugfix:
|
|
6
|
+
|
|
7
|
+
- Major changes to internals to better support thread-safe usage. Thanks to Kurtis for the bug report
|
|
8
|
+
|
|
9
|
+
## 1.12.5
|
|
10
|
+
|
|
11
|
+
Feature:
|
|
12
|
+
|
|
13
|
+
- Some broken APIs require a GET body, even though this is against HTTP spec. Added a `send_get_body` parameter like we already have for DELETE requests (thanks to Jan Schroeder for the request)
|
|
14
|
+
|
|
3
15
|
## 1.12.4
|
|
4
16
|
|
|
5
17
|
Bugfix:
|
data/docs/basic-usage.md
CHANGED
|
@@ -52,6 +52,12 @@ For `delete` requests whether an API can handle a body or not is undefined. The
|
|
|
52
52
|
delete :remove, "/people/:id", send_delete_body: true
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
In a similar way, although it's against the HTTP specification, you can force a GET request to send a request body if your API requires that:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
get :people_search, "/people/search", send_get_body: true
|
|
59
|
+
```
|
|
60
|
+
|
|
55
61
|
If an API returns an array of results and you have [will_paginate](https://rubygems.org/gems/will_paginate) installed then you can call the paginate method to return a particular page of the results (note: this doesn't reduce the load on the server, but it can help with pagination if you have a cached response).
|
|
56
62
|
|
|
57
63
|
```ruby
|
|
@@ -51,17 +51,22 @@ module Flexirest
|
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
# The keys passed to `.includes(...)` travel from this class-method call
|
|
55
|
+
# into the Request built by the subsequent finder call. Storing them in a
|
|
56
|
+
# class instance variable meant concurrent threads calling `.includes` on
|
|
57
|
+
# the same model shared (and corrupted) one slot, so they're held in
|
|
58
|
+
# thread-local storage keyed by the class instead.
|
|
54
59
|
def includes(*keys)
|
|
55
|
-
|
|
60
|
+
(Thread.current[:flexirest_include_associations] ||= {})[self] = keys
|
|
56
61
|
self
|
|
57
62
|
end
|
|
58
63
|
|
|
59
64
|
def _include_associations
|
|
60
|
-
|
|
65
|
+
(Thread.current[:flexirest_include_associations] ||= {})[self] || []
|
|
61
66
|
end
|
|
62
67
|
|
|
63
68
|
def _reset_include_associations!
|
|
64
|
-
|
|
69
|
+
(Thread.current[:flexirest_include_associations] ||= {}).delete(self)
|
|
65
70
|
end
|
|
66
71
|
|
|
67
72
|
def parse_date(*keys)
|
|
@@ -80,7 +85,6 @@ module Flexirest
|
|
|
80
85
|
def inherited(subclass)
|
|
81
86
|
subclass.instance_variable_set(:@_date_fields, [])
|
|
82
87
|
subclass.instance_variable_set(:@_associations, {})
|
|
83
|
-
subclass.instance_variable_set(:@_include_associations, [])
|
|
84
88
|
super
|
|
85
89
|
end
|
|
86
90
|
end
|
data/lib/flexirest/connection.rb
CHANGED
|
@@ -41,6 +41,18 @@ module Flexirest
|
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
def get_with_body(path, data, options={})
|
|
45
|
+
set_defaults(options)
|
|
46
|
+
make_safe_request(path) do
|
|
47
|
+
@session.get(path) do |req|
|
|
48
|
+
set_per_request_timeout(req, options) if options[:timeout]
|
|
49
|
+
req.headers = req.headers.merge(options[:headers])
|
|
50
|
+
req.body = data
|
|
51
|
+
sign_request(req, options[:api_auth])
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
44
56
|
def get(path, options={})
|
|
45
57
|
set_defaults(options)
|
|
46
58
|
make_safe_request(path) do
|
|
@@ -7,25 +7,28 @@ module Flexirest
|
|
|
7
7
|
Flexirest::Logger.debug " \033[1;4;32m#{name}\033[0m #{event.payload[:name]}" unless event.payload[:quiet]
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
+
# These counters accumulate per request and are reported/reset per
|
|
11
|
+
# controller action. Held per-thread so concurrent requests each track their
|
|
12
|
+
# own totals instead of sharing (and losing updates to) one class variable.
|
|
10
13
|
def self.time_spent=(value)
|
|
11
|
-
|
|
14
|
+
Thread.current[:flexirest_time_spent] = value
|
|
12
15
|
end
|
|
13
16
|
|
|
14
17
|
def self.time_spent
|
|
15
|
-
|
|
18
|
+
Thread.current[:flexirest_time_spent] ||= 0
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
def self.calls_made=(value)
|
|
19
|
-
|
|
22
|
+
Thread.current[:flexirest_calls_made] = value
|
|
20
23
|
end
|
|
21
24
|
|
|
22
25
|
def self.calls_made
|
|
23
|
-
|
|
26
|
+
Thread.current[:flexirest_calls_made] ||= 0
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
def self.reset
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
Thread.current[:flexirest_time_spent] = 0
|
|
31
|
+
Thread.current[:flexirest_calls_made] = 0
|
|
29
32
|
end
|
|
30
33
|
|
|
31
34
|
def logger
|
|
@@ -172,10 +172,18 @@ module Flexirest
|
|
|
172
172
|
# Creating JSON API header
|
|
173
173
|
module Headers
|
|
174
174
|
extend self
|
|
175
|
+
# `extend self` makes this module a single process-wide object, so the
|
|
176
|
+
# headers used while building a request must not be stashed in an ivar
|
|
177
|
+
# here — concurrent requests would overwrite each other's (and leak
|
|
178
|
+
# authentication headers across requests). Hold them per-thread instead.
|
|
175
179
|
def save(headers)
|
|
176
180
|
# Save headers used in a request for building lazy association
|
|
177
181
|
# loaders when parsing the response
|
|
178
|
-
|
|
182
|
+
Thread.current[:flexirest_jsonapi_headers] = headers
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def headers
|
|
186
|
+
Thread.current[:flexirest_jsonapi_headers]
|
|
179
187
|
end
|
|
180
188
|
end
|
|
181
189
|
|
|
@@ -185,8 +193,16 @@ module Flexirest
|
|
|
185
193
|
extend Flexirest::JsonAPIProxy::Helpers
|
|
186
194
|
ID_PFIX = '_id_'
|
|
187
195
|
|
|
196
|
+
# `extend self` makes Response a single shared object too. The resource
|
|
197
|
+
# class saved here is read again while building lazy association loaders
|
|
198
|
+
# during the same parse, so it must be per-thread: otherwise a concurrent
|
|
199
|
+
# parse of a different model would corrupt association resolution.
|
|
188
200
|
def save_resource_class(object)
|
|
189
|
-
|
|
201
|
+
Thread.current[:flexirest_jsonapi_resource_class] = object.is_a?(Class) ? object : object.class
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def resource_class
|
|
205
|
+
Thread.current[:flexirest_jsonapi_resource_class]
|
|
190
206
|
end
|
|
191
207
|
|
|
192
208
|
def parse(body, object)
|
|
@@ -387,7 +403,7 @@ module Flexirest
|
|
|
387
403
|
|
|
388
404
|
def find_association_class(base, name)
|
|
389
405
|
stack = base + [name]
|
|
390
|
-
klass =
|
|
406
|
+
klass = resource_class
|
|
391
407
|
|
|
392
408
|
until stack.empty?
|
|
393
409
|
shift = stack.shift
|
|
@@ -424,7 +440,7 @@ module Flexirest
|
|
|
424
440
|
|
|
425
441
|
# Also add the previous request's header, which may contain
|
|
426
442
|
# crucial authentication headers (or so), to connect with the service
|
|
427
|
-
request.headers =
|
|
443
|
+
request.headers = Flexirest::JsonAPIProxy::Headers.headers
|
|
428
444
|
request.url = request.forced_url = url
|
|
429
445
|
|
|
430
446
|
Flexirest::LazyAssociationLoader.new(name, url, request)
|
data/lib/flexirest/logger.rb
CHANGED
|
@@ -2,18 +2,25 @@ module Flexirest
|
|
|
2
2
|
class Logger
|
|
3
3
|
@logfile = nil
|
|
4
4
|
@messages = []
|
|
5
|
+
# Guards the shared in-memory buffer / logfile handle below, which are
|
|
6
|
+
# written on every log call during request handling. Without this,
|
|
7
|
+
# concurrent requests racing on `@messages << ...` (or a shared IO) can
|
|
8
|
+
# interleave and lose or corrupt entries.
|
|
9
|
+
@mutex = Mutex.new
|
|
5
10
|
|
|
6
11
|
def self.logfile=(value)
|
|
7
|
-
@logfile = value
|
|
12
|
+
@mutex.synchronize { @logfile = value }
|
|
8
13
|
end
|
|
9
14
|
|
|
10
15
|
def self.messages
|
|
11
|
-
@messages
|
|
16
|
+
@mutex.synchronize { @messages.dup }
|
|
12
17
|
end
|
|
13
18
|
|
|
14
19
|
def self.reset!
|
|
15
|
-
@
|
|
16
|
-
|
|
20
|
+
@mutex.synchronize do
|
|
21
|
+
@logfile = nil
|
|
22
|
+
@messages = []
|
|
23
|
+
end
|
|
17
24
|
end
|
|
18
25
|
|
|
19
26
|
def self.level
|
|
@@ -27,65 +34,52 @@ module Flexirest
|
|
|
27
34
|
def self.debug(message)
|
|
28
35
|
if defined?(Rails) && Rails.logger.present?
|
|
29
36
|
Rails.logger.debug(message)
|
|
30
|
-
elsif @logfile
|
|
31
|
-
if @logfile.is_a?(String)
|
|
32
|
-
File.open(@logfile, "a") do |f|
|
|
33
|
-
f << "#{message}\n"
|
|
34
|
-
end
|
|
35
|
-
else
|
|
36
|
-
@logfile << "#{message}\n"
|
|
37
|
-
end
|
|
38
37
|
else
|
|
39
|
-
|
|
38
|
+
write_fallback(message)
|
|
40
39
|
end
|
|
41
40
|
end
|
|
42
41
|
|
|
43
42
|
def self.info(message)
|
|
44
43
|
if defined?(Rails) && Rails.logger.present?
|
|
45
44
|
Rails.logger.info(message)
|
|
46
|
-
elsif @logfile
|
|
47
|
-
if @logfile.is_a?(String)
|
|
48
|
-
File.open(@logfile, "a") do |f|
|
|
49
|
-
f << "#{message}\n"
|
|
50
|
-
end
|
|
51
|
-
else
|
|
52
|
-
@logfile << "#{message}\n"
|
|
53
|
-
end
|
|
54
45
|
else
|
|
55
|
-
|
|
46
|
+
write_fallback(message)
|
|
56
47
|
end
|
|
57
48
|
end
|
|
58
49
|
|
|
59
50
|
def self.warn(message)
|
|
60
51
|
if defined?(Rails) && Rails.logger.present?
|
|
61
52
|
Rails.logger.warn(message)
|
|
62
|
-
elsif @logfile
|
|
63
|
-
if @logfile.is_a?(String)
|
|
64
|
-
File.open(@logfile, "a") do |f|
|
|
65
|
-
f << "#{message}\n"
|
|
66
|
-
end
|
|
67
|
-
else
|
|
68
|
-
@logfile << "#{message}\n"
|
|
69
|
-
end
|
|
70
53
|
else
|
|
71
|
-
|
|
54
|
+
write_fallback(message)
|
|
72
55
|
end
|
|
73
56
|
end
|
|
74
57
|
|
|
75
58
|
def self.error(message)
|
|
76
59
|
if defined?(Rails) && Rails.logger.present?
|
|
77
60
|
Rails.logger.error(message)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
61
|
+
else
|
|
62
|
+
write_fallback(message)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Writes to the configured logfile, or falls back to the in-memory buffer.
|
|
67
|
+
# Synchronised because both destinations are shared across threads.
|
|
68
|
+
def self.write_fallback(message)
|
|
69
|
+
@mutex.synchronize do
|
|
70
|
+
if @logfile
|
|
71
|
+
if @logfile.is_a?(String)
|
|
72
|
+
File.open(@logfile, "a") do |f|
|
|
73
|
+
f << "#{message}\n"
|
|
74
|
+
end
|
|
75
|
+
else
|
|
76
|
+
@logfile << "#{message}\n"
|
|
82
77
|
end
|
|
83
78
|
else
|
|
84
|
-
@
|
|
79
|
+
@messages << message
|
|
85
80
|
end
|
|
86
|
-
else
|
|
87
|
-
@messages << message
|
|
88
81
|
end
|
|
89
82
|
end
|
|
83
|
+
private_class_method :write_fallback
|
|
90
84
|
end
|
|
91
85
|
end
|
data/lib/flexirest/proxy_base.rb
CHANGED
|
@@ -2,10 +2,19 @@ require "uri"
|
|
|
2
2
|
|
|
3
3
|
module Flexirest
|
|
4
4
|
class ProxyBase
|
|
5
|
-
cattr_accessor :mappings, :request, :original_handler
|
|
6
|
-
cattr_accessor :original_body, :original_get_params, :original_post_params, :original_url
|
|
7
|
-
|
|
8
5
|
module ClassMethods
|
|
6
|
+
# Per-request state used while handling a proxied request is stored in
|
|
7
|
+
# thread-local storage (keyed by the proxy class) rather than on the
|
|
8
|
+
# class itself. The DSL is implemented as class methods run via
|
|
9
|
+
# class_eval, so historically "the current request" was stored in class
|
|
10
|
+
# instance variables/class variables, which are shared across every
|
|
11
|
+
# thread calling through the same proxy class. That caused request data
|
|
12
|
+
# to be dropped or corrupted under concurrency. Keeping it per-thread
|
|
13
|
+
# isolates concurrent requests while preserving the existing DSL.
|
|
14
|
+
def _proxy_state
|
|
15
|
+
states = (Thread.current[:flexirest_proxy_state] ||= {})
|
|
16
|
+
states[self] ||= {}
|
|
17
|
+
end
|
|
9
18
|
def get(match, &block)
|
|
10
19
|
add_mapping(:get, match, block)
|
|
11
20
|
end
|
|
@@ -41,33 +50,34 @@ module Flexirest
|
|
|
41
50
|
end
|
|
42
51
|
|
|
43
52
|
def body(value = nil)
|
|
44
|
-
|
|
45
|
-
|
|
53
|
+
_proxy_state[:body] = value if value
|
|
54
|
+
_proxy_state[:body]
|
|
46
55
|
end
|
|
47
56
|
|
|
48
57
|
def url(value = nil)
|
|
49
|
-
|
|
50
|
-
|
|
58
|
+
_proxy_state[:url] = value if value
|
|
59
|
+
_proxy_state[:url]
|
|
51
60
|
end
|
|
52
61
|
|
|
53
62
|
def get_params(value = nil)
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
_proxy_state[:get_params] = value if value
|
|
64
|
+
_proxy_state[:get_params]
|
|
56
65
|
end
|
|
57
66
|
|
|
58
67
|
def post_params(value = nil)
|
|
59
|
-
|
|
60
|
-
|
|
68
|
+
_proxy_state[:post_params] = value if value
|
|
69
|
+
_proxy_state[:post_params]
|
|
61
70
|
end
|
|
62
71
|
|
|
63
72
|
def params(value = nil)
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
_proxy_state[:params] = value if value
|
|
74
|
+
_proxy_state[:params]
|
|
66
75
|
end
|
|
67
76
|
|
|
68
77
|
def passthrough
|
|
69
78
|
rebuild_request
|
|
70
|
-
|
|
79
|
+
state = _proxy_state
|
|
80
|
+
state[:original_handler].call(state[:request])
|
|
71
81
|
end
|
|
72
82
|
|
|
73
83
|
def result_is_json_or_unspecified?(result)
|
|
@@ -93,56 +103,73 @@ module Flexirest
|
|
|
93
103
|
end
|
|
94
104
|
|
|
95
105
|
def rebuild_request
|
|
96
|
-
|
|
97
|
-
|
|
106
|
+
state = _proxy_state
|
|
107
|
+
request = state[:request]
|
|
108
|
+
if state[:url] != state[:original_url]
|
|
109
|
+
request.forced_url = request.url = state[:url]
|
|
98
110
|
end
|
|
99
|
-
if
|
|
100
|
-
|
|
101
|
-
elsif
|
|
102
|
-
|
|
103
|
-
|
|
111
|
+
if state[:body] != state[:original_body]
|
|
112
|
+
request.body = state[:body]
|
|
113
|
+
elsif state[:post_params] != state[:original_post_params]
|
|
114
|
+
request.body = nil
|
|
115
|
+
request.prepare_request_body(state[:post_params])
|
|
104
116
|
end
|
|
105
|
-
if
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
117
|
+
if state[:get_params] != state[:original_get_params]
|
|
118
|
+
request.get_params = state[:get_params]
|
|
119
|
+
request.prepare_url
|
|
120
|
+
request.append_get_parameters
|
|
109
121
|
end
|
|
110
122
|
end
|
|
111
123
|
|
|
112
124
|
def handle(request, &block)
|
|
113
|
-
|
|
114
|
-
|
|
125
|
+
# Preserve any state already in flight on this thread (e.g. a proxied
|
|
126
|
+
# request that itself triggers another request through the same proxy
|
|
127
|
+
# class) and restore it when we're done, so nested calls can't clobber
|
|
128
|
+
# each other.
|
|
129
|
+
states = (Thread.current[:flexirest_proxy_state] ||= {})
|
|
130
|
+
previous_state = states[self]
|
|
131
|
+
|
|
132
|
+
state = states[self] = {}
|
|
133
|
+
state[:request] = request
|
|
134
|
+
state[:original_handler] = block
|
|
115
135
|
|
|
116
|
-
|
|
117
|
-
|
|
136
|
+
state[:original_body] = request.body
|
|
137
|
+
state[:body] = state[:original_body].dup
|
|
118
138
|
|
|
119
|
-
|
|
120
|
-
|
|
139
|
+
state[:original_get_params] = request.get_params
|
|
140
|
+
state[:get_params] = state[:original_get_params].dup
|
|
121
141
|
|
|
122
|
-
|
|
123
|
-
|
|
142
|
+
state[:original_post_params] = request.post_params
|
|
143
|
+
state[:post_params] = (state[:original_post_params] || {}).dup
|
|
124
144
|
|
|
125
|
-
|
|
126
|
-
|
|
145
|
+
state[:original_url] = request.url
|
|
146
|
+
state[:url] = state[:original_url].dup
|
|
127
147
|
|
|
128
148
|
if mapping = find_mapping_for_current_request
|
|
129
149
|
self.class_eval(&mapping.block)
|
|
130
150
|
else
|
|
131
151
|
passthrough
|
|
132
152
|
end
|
|
153
|
+
ensure
|
|
154
|
+
if previous_state
|
|
155
|
+
states[self] = previous_state
|
|
156
|
+
else
|
|
157
|
+
states.delete(self)
|
|
158
|
+
end
|
|
133
159
|
end
|
|
134
160
|
|
|
135
161
|
def find_mapping_for_current_request
|
|
136
|
-
|
|
162
|
+
state = _proxy_state
|
|
163
|
+
uri = URI.parse(state[:original_url])
|
|
137
164
|
@mappings ||= []
|
|
138
|
-
|
|
165
|
+
state[:params] = {}
|
|
139
166
|
@mappings.each do |mapping|
|
|
140
167
|
match = mapping.match
|
|
141
|
-
if (match_data = uri.path.match(match)) &&
|
|
168
|
+
if (match_data = uri.path.match(match)) && state[:request].http_method.to_sym == mapping.http_method
|
|
142
169
|
matches = match_data.to_a
|
|
143
170
|
matches.shift
|
|
144
171
|
matches.each_with_index do |value, index|
|
|
145
|
-
|
|
172
|
+
state[:params][mapping.param_keys[index]] = value
|
|
146
173
|
end
|
|
147
174
|
return mapping
|
|
148
175
|
end
|
data/lib/flexirest/request.rb
CHANGED
|
@@ -361,10 +361,10 @@ module Flexirest
|
|
|
361
361
|
if @explicit_parameters
|
|
362
362
|
params = @explicit_parameters
|
|
363
363
|
end
|
|
364
|
-
if http_method == :get || (http_method == :delete && !@method[:options][:send_delete_body] && proxy != :json_api)
|
|
364
|
+
if (http_method == :get && !@method[:options][:send_get_body]) || (http_method == :delete && !@method[:options][:send_delete_body] && proxy != :json_api)
|
|
365
365
|
@get_params = default_params.merge(params || {})
|
|
366
366
|
@post_params = nil
|
|
367
|
-
elsif http_method == :delete && @method[:options][:send_delete_body]
|
|
367
|
+
elsif (http_method == :get && @method[:options][:send_get_body]) || (http_method == :delete && @method[:options][:send_delete_body])
|
|
368
368
|
@post_params = default_params.merge(params || {})
|
|
369
369
|
@get_params = {}
|
|
370
370
|
elsif params.is_a? String
|
|
@@ -476,7 +476,7 @@ module Flexirest
|
|
|
476
476
|
|
|
477
477
|
headers["Accept"] ||= "application/vnd.api+json"
|
|
478
478
|
JsonAPIProxy::Headers.save(headers)
|
|
479
|
-
elsif http_method == :get || (http_method == :delete && !@method[:options][:send_delete_body])
|
|
479
|
+
elsif (http_method == :get && !@method[:options][:send_get_body]) || (http_method == :delete && !@method[:options][:send_delete_body])
|
|
480
480
|
if request_body_type == :form_encoded
|
|
481
481
|
headers["Content-Type"] ||= "application/x-www-form-urlencoded; charset=utf-8"
|
|
482
482
|
elsif request_body_type == :form_multipart
|
|
@@ -607,16 +607,17 @@ module Flexirest
|
|
|
607
607
|
request_options[:timeout] = @method[:options][:timeout]
|
|
608
608
|
end
|
|
609
609
|
|
|
610
|
-
|
|
611
|
-
when :get
|
|
610
|
+
if http_method == :get && !@method[:options][:send_get_body]
|
|
612
611
|
response = connection.get(@url, request_options)
|
|
613
|
-
|
|
612
|
+
elsif http_method == :get
|
|
613
|
+
response = connection.get_with_body(@url, @body, request_options)
|
|
614
|
+
elsif http_method == :put
|
|
614
615
|
response = connection.put(@url, @body, request_options)
|
|
615
|
-
|
|
616
|
+
elsif http_method == :post
|
|
616
617
|
response = connection.post(@url, @body, request_options)
|
|
617
|
-
|
|
618
|
+
elsif http_method == :patch
|
|
618
619
|
response = connection.patch(@url, @body, request_options)
|
|
619
|
-
|
|
620
|
+
elsif http_method == :delete
|
|
620
621
|
response = connection.delete(@url, @body, request_options)
|
|
621
622
|
else
|
|
622
623
|
raise InvalidRequestException.new("Invalid method #{http_method}")
|
data/lib/flexirest/version.rb
CHANGED
|
@@ -84,6 +84,37 @@ describe "Has Many Associations" do
|
|
|
84
84
|
end
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
+
describe "includes association thread safety" do
|
|
88
|
+
it "keeps .includes state isolated across concurrent threads" do
|
|
89
|
+
thread_count = 10
|
|
90
|
+
|
|
91
|
+
# All threads set their own includes, then wait until every thread has done
|
|
92
|
+
# so before reading it back. Before the fix, the shared class instance
|
|
93
|
+
# variable meant each thread read whichever value was written last.
|
|
94
|
+
count = 0
|
|
95
|
+
mutex = Mutex.new
|
|
96
|
+
cond = ConditionVariable.new
|
|
97
|
+
results = {}
|
|
98
|
+
|
|
99
|
+
threads = thread_count.times.map do |i|
|
|
100
|
+
Thread.new do
|
|
101
|
+
AssociationExampleBase.includes(:"assoc_#{i}")
|
|
102
|
+
mutex.synchronize do
|
|
103
|
+
count += 1
|
|
104
|
+
cond.broadcast
|
|
105
|
+
cond.wait(mutex) while count < thread_count
|
|
106
|
+
end
|
|
107
|
+
results[i] = AssociationExampleBase._include_associations
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
threads.each { |t| t.join(5) }
|
|
111
|
+
|
|
112
|
+
thread_count.times do |i|
|
|
113
|
+
expect(results[i]).to eq([:"assoc_#{i}"])
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
87
118
|
describe "Has One Associations" do
|
|
88
119
|
let(:subject) {AssociationExampleBase.new}
|
|
89
120
|
|
data/spec/lib/json_api_spec.rb
CHANGED
|
@@ -567,3 +567,64 @@ describe 'JSON API' do
|
|
|
567
567
|
end
|
|
568
568
|
end
|
|
569
569
|
end
|
|
570
|
+
|
|
571
|
+
# A simple N-party rendezvous forcing concurrent parses to interleave, so the
|
|
572
|
+
# per-request state the JSON:API proxy stashes (resource class + headers) is
|
|
573
|
+
# provably isolated per thread rather than shared on the singleton modules.
|
|
574
|
+
class JsonAPIThreadBarrier
|
|
575
|
+
def initialize(parties)
|
|
576
|
+
@parties = parties
|
|
577
|
+
@count = 0
|
|
578
|
+
@mutex = Mutex.new
|
|
579
|
+
@cond = ConditionVariable.new
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
def wait
|
|
583
|
+
@mutex.synchronize do
|
|
584
|
+
@count += 1
|
|
585
|
+
if @count >= @parties
|
|
586
|
+
@cond.broadcast
|
|
587
|
+
else
|
|
588
|
+
@cond.wait(@mutex) while @count < @parties
|
|
589
|
+
end
|
|
590
|
+
end
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
describe Flexirest::JsonAPIProxy do
|
|
595
|
+
it 'keeps the parsed resource class isolated across concurrent threads' do
|
|
596
|
+
classes = [JsonAPIExampleArticle, JsonAPIAssociationExampleTag, JsonAPIAssociationExampleAuthor]
|
|
597
|
+
thread_count = 9
|
|
598
|
+
barrier = JsonAPIThreadBarrier.new(thread_count)
|
|
599
|
+
|
|
600
|
+
threads = thread_count.times.map do |i|
|
|
601
|
+
klass = classes[i % classes.size]
|
|
602
|
+
Thread.new do
|
|
603
|
+
Flexirest::JsonAPIProxy::Response.save_resource_class(klass)
|
|
604
|
+
# All threads have now saved a class; if it were shared they'd all read
|
|
605
|
+
# whichever was written last.
|
|
606
|
+
barrier.wait
|
|
607
|
+
[i, Flexirest::JsonAPIProxy::Response.resource_class]
|
|
608
|
+
end
|
|
609
|
+
end
|
|
610
|
+
results = threads.to_h(&:value)
|
|
611
|
+
|
|
612
|
+
thread_count.times { |i| expect(results[i]).to eq(classes[i % classes.size]) }
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
it 'keeps the saved request headers isolated across concurrent threads' do
|
|
616
|
+
thread_count = 10
|
|
617
|
+
barrier = JsonAPIThreadBarrier.new(thread_count)
|
|
618
|
+
|
|
619
|
+
threads = thread_count.times.map do |i|
|
|
620
|
+
Thread.new do
|
|
621
|
+
Flexirest::JsonAPIProxy::Headers.save('X-Marker' => i.to_s)
|
|
622
|
+
barrier.wait
|
|
623
|
+
[i, Flexirest::JsonAPIProxy::Headers.headers]
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
results = threads.to_h(&:value)
|
|
627
|
+
|
|
628
|
+
thread_count.times { |i| expect(results[i]).to eq('X-Marker' => i.to_s) }
|
|
629
|
+
end
|
|
630
|
+
end
|
data/spec/lib/proxy_spec.rb
CHANGED
|
@@ -2,6 +2,29 @@ require 'spec_helper'
|
|
|
2
2
|
require 'active_support/core_ext/hash'
|
|
3
3
|
require 'base64'
|
|
4
4
|
|
|
5
|
+
# A simple N-party rendezvous used to force concurrent proxy requests to be
|
|
6
|
+
# interleaved inside `handle` at the same time, deterministically reproducing
|
|
7
|
+
# the thread-safety bug where per-request state was stored on the class.
|
|
8
|
+
class ProxyTestBarrier
|
|
9
|
+
def initialize(parties)
|
|
10
|
+
@parties = parties
|
|
11
|
+
@count = 0
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
@cond = ConditionVariable.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def wait
|
|
17
|
+
@mutex.synchronize do
|
|
18
|
+
@count += 1
|
|
19
|
+
if @count >= @parties
|
|
20
|
+
@cond.broadcast
|
|
21
|
+
else
|
|
22
|
+
@cond.wait(@mutex) while @count < @parties
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
5
28
|
class ProxyExample < Flexirest::ProxyBase
|
|
6
29
|
get "/all" do
|
|
7
30
|
url.gsub!("/all", "/getAll")
|
|
@@ -72,6 +95,22 @@ class ProxyExample < Flexirest::ProxyBase
|
|
|
72
95
|
get "/fake" do
|
|
73
96
|
render "{\"id\":1234}"
|
|
74
97
|
end
|
|
98
|
+
|
|
99
|
+
def self.thread_barrier=(barrier)
|
|
100
|
+
@thread_barrier = barrier
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def self.thread_barrier
|
|
104
|
+
@thread_barrier
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
get "/thread_test/:marker" do
|
|
108
|
+
url "/thread_result/#{params[:marker]}"
|
|
109
|
+
# Pause every concurrent request here, after it has set its own URL but
|
|
110
|
+
# before it is sent, so all threads are inside `handle` simultaneously.
|
|
111
|
+
ProxyExample.thread_barrier.wait
|
|
112
|
+
passthrough
|
|
113
|
+
end
|
|
75
114
|
end
|
|
76
115
|
|
|
77
116
|
class ProxyClientExample < Flexirest::Base
|
|
@@ -91,6 +130,7 @@ class ProxyClientExample < Flexirest::Base
|
|
|
91
130
|
get :not_proxied, "/not_proxied"
|
|
92
131
|
delete :remove, "/remove"
|
|
93
132
|
get :hal_test, "/hal_test/:id"
|
|
133
|
+
get :thread_test, "/thread_test/:marker"
|
|
94
134
|
end
|
|
95
135
|
|
|
96
136
|
describe Flexirest::Base do
|
|
@@ -200,6 +240,31 @@ describe Flexirest::Base do
|
|
|
200
240
|
expect(ProxyClientExample.hal_test(id:1).test.result).to eq(true)
|
|
201
241
|
end
|
|
202
242
|
|
|
243
|
+
it "keeps proxied request data isolated across concurrent threads" do
|
|
244
|
+
thread_count = 10
|
|
245
|
+
ProxyExample.thread_barrier = ProxyTestBarrier.new(thread_count)
|
|
246
|
+
|
|
247
|
+
requested_paths = Queue.new
|
|
248
|
+
allow_any_instance_of(Flexirest::Connection).to receive(:get) do |_connection, path, _options|
|
|
249
|
+
requested_paths << path
|
|
250
|
+
::FaradayResponseMock.new(OpenStruct.new(body: "{\"result\":true}", status: 200, response_headers: {}))
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
threads = thread_count.times.map do |i|
|
|
254
|
+
Thread.new { ProxyClientExample.thread_test(marker: i) }
|
|
255
|
+
end
|
|
256
|
+
threads.each { |t| t.join(5) }
|
|
257
|
+
|
|
258
|
+
paths = []
|
|
259
|
+
paths << requested_paths.pop until requested_paths.empty?
|
|
260
|
+
|
|
261
|
+
# Each thread must reach the server with its own marker. Before the fix the
|
|
262
|
+
# shared class-level state meant every thread sent whichever URL was written
|
|
263
|
+
# last, so the paths collided instead of covering 0..9.
|
|
264
|
+
expected = (0...thread_count).map { |i| "/thread_result/#{i}" }
|
|
265
|
+
expect(paths.sort).to eq(expected.sort)
|
|
266
|
+
end
|
|
267
|
+
|
|
203
268
|
it "properly passes basic HTTP auth credentials" do
|
|
204
269
|
host, credentials, url_path = 'www.example.com', 'user:pass', '/getAll?id=1'
|
|
205
270
|
ProxyClientExample.base_url "http://#{credentials}@#{host}"
|
data/spec/lib/request_spec.rb
CHANGED
|
@@ -54,6 +54,7 @@ describe Flexirest::Request do
|
|
|
54
54
|
put :conversion_child, "/put/:id", parse_fields: [:converted_child]
|
|
55
55
|
delete :remove, "/remove/:id"
|
|
56
56
|
delete :remove_body, "/remove/:id", send_delete_body: true
|
|
57
|
+
get :get_body, "/get-body", send_get_body: true
|
|
57
58
|
get :hal, "/hal", fake:"{\"_links\":{\"child\": {\"href\": \"/child/1\"}, \"other\": {\"href\": \"/other/1\"}, \"cars\":[{\"href\": \"/car/1\", \"name\":\"car1\"}, {\"href\": \"/car/2\", \"name\":\"car2\"}, {\"href\": \"/car/not-embed\", \"name\":\"car_not_embed\"} ], \"lazy\": {\"href\": \"/lazy/load\"}, \"invalid\": [{\"href\": \"/invalid/1\"}]}, \"_embedded\":{\"other\":{\"name\":\"Jane\"},\"child\":{\"name\":\"Billy\"}, \"cars\":[{\"_links\": {\"self\": {\"href\": \"/car/1\"} }, \"make\": \"Bugatti\", \"model\": \"Veyron\"}, {\"_links\": {\"self\": {\"href\": \"/car/2\"} }, \"make\": \"Ferrari\", \"model\": \"F458 Italia\"} ], \"invalid\": [{\"present\":true, \"_links\": {} } ] } }", has_many:{other:ExampleOtherClient}
|
|
58
59
|
get :fake_object, "/fake", fake:"{\"result\":true, \"list\":[1,2,3,{\"test\":true}], \"child\":{\"grandchild\":{\"test\":true}}}"
|
|
59
60
|
get :fake_proc_object, "/fake", fake:->(request) { "{\"result\":#{request.get_params[:id]}}" }
|
|
@@ -451,6 +452,11 @@ describe Flexirest::Request do
|
|
|
451
452
|
ExampleClient.all
|
|
452
453
|
end
|
|
453
454
|
|
|
455
|
+
it "should get an HTTP connection when called and call get with a body if send_get_body is specified" do
|
|
456
|
+
expect_any_instance_of(Flexirest::Connection).to receive(:get_with_body).with("/get-body", "something=else", an_instance_of(Hash)).and_return(::FaradayResponseMock.new(OpenStruct.new(body:'{"result":true}', response_headers:{})))
|
|
457
|
+
ExampleClient.get_body(something: "else")
|
|
458
|
+
end
|
|
459
|
+
|
|
454
460
|
it "should get an HTTP connection when called and call delete on it" do
|
|
455
461
|
expect_any_instance_of(Flexirest::Connection).to receive(:delete).with("/remove/1", "", an_instance_of(Hash)).and_return(::FaradayResponseMock.new(OpenStruct.new(body:'{"result":true}', response_headers:{})))
|
|
456
462
|
ExampleClient.remove(id:1)
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: flexirest
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andy Jeffries
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: bundler
|
|
@@ -392,7 +391,6 @@ licenses:
|
|
|
392
391
|
- MIT
|
|
393
392
|
metadata:
|
|
394
393
|
source_code_uri: https://github.com/flexirest/flexirest
|
|
395
|
-
post_install_message:
|
|
396
394
|
rdoc_options: []
|
|
397
395
|
require_paths:
|
|
398
396
|
- lib
|
|
@@ -407,8 +405,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
407
405
|
- !ruby/object:Gem::Version
|
|
408
406
|
version: '0'
|
|
409
407
|
requirements: []
|
|
410
|
-
rubygems_version: 3.
|
|
411
|
-
signing_key:
|
|
408
|
+
rubygems_version: 3.6.9
|
|
412
409
|
specification_version: 4
|
|
413
410
|
summary: This gem is for accessing REST services in a flexible way. ActiveResource
|
|
414
411
|
already exists for this, but it doesn't work where the resource naming doesn't follow
|