patient_http 1.1.1 → 1.2.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 +32 -0
- data/README.md +64 -3
- data/VERSION +1 -1
- data/lib/patient_http/client.rb +10 -12
- data/lib/patient_http/client_pool.rb +23 -8
- data/lib/patient_http/configuration.rb +78 -1
- data/lib/patient_http/http_headers.rb +7 -0
- data/lib/patient_http/lifecycle_manager.rb +20 -8
- data/lib/patient_http/outgoing_request.rb +72 -0
- data/lib/patient_http/payload.rb +1 -1
- data/lib/patient_http/payload_store/redis_store.rb +4 -1
- data/lib/patient_http/processor.rb +290 -109
- data/lib/patient_http/redirect_error.rb +5 -0
- data/lib/patient_http/request.rb +26 -3
- data/lib/patient_http/request_error.rb +1 -1
- data/lib/patient_http/request_preparer.rb +52 -0
- data/lib/patient_http/request_task.rb +16 -7
- data/lib/patient_http/request_template.rb +9 -3
- data/lib/patient_http/response.rb +3 -0
- data/lib/patient_http/response_reader.rb +16 -4
- data/lib/patient_http/synchronous_executor.rb +53 -45
- data/lib/patient_http.rb +16 -2
- metadata +3 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PatientHttp
|
|
4
|
+
# Prepares a {Request} to be sent: resolves any secret references, sets the
|
|
5
|
+
# send-time headers (x-request-id and the default user-agent), and invokes any
|
|
6
|
+
# preprocessors attached to the request.
|
|
7
|
+
class RequestPreparer
|
|
8
|
+
# Raised when a request references a preprocessor name that is not registered.
|
|
9
|
+
class PreprocessorNotFoundError < StandardError; end
|
|
10
|
+
|
|
11
|
+
# @param config [Configuration] the configuration holding secrets and preprocessors
|
|
12
|
+
def initialize(config)
|
|
13
|
+
@config = config
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Prepare a request for sending.
|
|
17
|
+
#
|
|
18
|
+
# Secret references in the headers and query params are resolved first, then the
|
|
19
|
+
# x-request-id and default user-agent headers are set, and finally each
|
|
20
|
+
# preprocessor attached to the request is invoked in order with the outgoing
|
|
21
|
+
# request. Each preprocessor sees any changes made by the ones before it.
|
|
22
|
+
#
|
|
23
|
+
# @param request [Request] the request to prepare
|
|
24
|
+
# @param request_id [String] unique request identifier set as the x-request-id header
|
|
25
|
+
# @return [OutgoingRequest] the outgoing request with the final URL and headers
|
|
26
|
+
# @raise [PreprocessorNotFoundError] if the request references an unregistered preprocessor
|
|
27
|
+
def prepare(request, request_id)
|
|
28
|
+
headers = @config.secret_manager.resolve_headers(request.headers.to_h)
|
|
29
|
+
headers["x-request-id"] = request_id
|
|
30
|
+
headers["user-agent"] ||= @config.user_agent if @config.user_agent
|
|
31
|
+
url = @config.secret_manager.resolve_url(request.url, request.secret_params)
|
|
32
|
+
|
|
33
|
+
outgoing = OutgoingRequest.new(
|
|
34
|
+
http_method: request.http_method,
|
|
35
|
+
url: url,
|
|
36
|
+
headers: HttpHeaders.new(headers),
|
|
37
|
+
body: request.body
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
request.preprocessors.each do |name|
|
|
41
|
+
preprocessor = @config.preprocessor(name)
|
|
42
|
+
unless preprocessor
|
|
43
|
+
raise PreprocessorNotFoundError.new("No preprocessor registered for #{name.inspect}")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
preprocessor.call(outgoing)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
outgoing
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -92,6 +92,15 @@ module PatientHttp
|
|
|
92
92
|
@started_at = monotonic_time
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
+
# Return true if the task has started processing (i.e. {#started!} was
|
|
96
|
+
# called). Used to keep observer request_start/request_end notifications
|
|
97
|
+
# balanced when a task is re-enqueued during shutdown.
|
|
98
|
+
#
|
|
99
|
+
# @return [Boolean]
|
|
100
|
+
def started?
|
|
101
|
+
!@started_at.nil?
|
|
102
|
+
end
|
|
103
|
+
|
|
95
104
|
# Returns the wall clock time when the task was enqueued.
|
|
96
105
|
#
|
|
97
106
|
# @return [Time, nil] The enqueued time or nil if not enqueued.
|
|
@@ -213,12 +222,11 @@ module PatientHttp
|
|
|
213
222
|
# Resolve the redirect URL (handle relative URLs)
|
|
214
223
|
redirect_url = resolve_redirect_url(location)
|
|
215
224
|
|
|
216
|
-
# Strip sensitive headers on cross-origin redirects to
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
end
|
|
225
|
+
# Strip sensitive headers and preprocessors on cross-origin redirects to
|
|
226
|
+
# prevent credential leakage
|
|
227
|
+
cross_origin = cross_origin?(request.url, redirect_url)
|
|
228
|
+
redirect_headers = cross_origin ? request.headers.except(*SENSITIVE_HEADERS) : request.headers
|
|
229
|
+
redirect_preprocessors = cross_origin ? [] : request.preprocessors
|
|
222
230
|
|
|
223
231
|
# Create a new request for the redirect
|
|
224
232
|
redirect_request = Request.new(
|
|
@@ -227,7 +235,8 @@ module PatientHttp
|
|
|
227
235
|
headers: redirect_headers,
|
|
228
236
|
body: redirect_body,
|
|
229
237
|
timeout: request.timeout,
|
|
230
|
-
max_redirects: request.max_redirects
|
|
238
|
+
max_redirects: request.max_redirects,
|
|
239
|
+
preprocessors: redirect_preprocessors
|
|
231
240
|
)
|
|
232
241
|
|
|
233
242
|
redirect_task_id = "#{id.split("/").first}/#{@redirects.size + 2}"
|
|
@@ -32,11 +32,14 @@ module PatientHttp
|
|
|
32
32
|
# @param headers [Hash] Default headers for all requests
|
|
33
33
|
# @param params [Hash, nil] Default query parameters to add to all requests
|
|
34
34
|
# @param timeout [Float] Default request timeout in seconds
|
|
35
|
-
|
|
35
|
+
# @param preprocessors [String, Symbol, Array<String, Symbol>, nil] Default preprocessors
|
|
36
|
+
# to apply to all requests
|
|
37
|
+
def initialize(base_url: nil, headers: {}, params: nil, timeout: 30, preprocessors: nil)
|
|
36
38
|
@base_url = base_url
|
|
37
39
|
@headers = HttpHeaders.new(headers)
|
|
38
40
|
@params = params
|
|
39
41
|
@timeout = timeout
|
|
42
|
+
@preprocessors = preprocessors
|
|
40
43
|
end
|
|
41
44
|
|
|
42
45
|
# Build an async HTTP request. Returns a Request object.
|
|
@@ -47,8 +50,10 @@ module PatientHttp
|
|
|
47
50
|
# @param json [Object, nil] JSON object to serialize (cannot use with body)
|
|
48
51
|
# @param headers [Hash] additional headers to merge with client headers
|
|
49
52
|
# @param params [Hash, nil] query parameters to add to URL
|
|
53
|
+
# @param preprocessors [String, Symbol, Array<String, Symbol>, nil] preprocessors to apply
|
|
54
|
+
# to the request (overrides the template default)
|
|
50
55
|
# @return [Request] request object
|
|
51
|
-
def request(method, uri, body: nil, json: nil, headers: nil, params: nil, timeout: nil)
|
|
56
|
+
def request(method, uri, body: nil, json: nil, headers: nil, params: nil, timeout: nil, preprocessors: nil)
|
|
52
57
|
full_uri = @base_url ? URI.join(@base_url, uri.to_s) : URI(uri)
|
|
53
58
|
|
|
54
59
|
merged_headers = headers&.any? ? @headers.merge(headers) : @headers
|
|
@@ -62,7 +67,8 @@ module PatientHttp
|
|
|
62
67
|
body: body,
|
|
63
68
|
json: json,
|
|
64
69
|
params: merged_params,
|
|
65
|
-
timeout: timeout || @timeout
|
|
70
|
+
timeout: timeout || @timeout,
|
|
71
|
+
preprocessors: preprocessors || @preprocessors
|
|
66
72
|
)
|
|
67
73
|
end
|
|
68
74
|
|
|
@@ -12,6 +12,9 @@ module PatientHttp
|
|
|
12
12
|
# @return [Integer] HTTP status code
|
|
13
13
|
attr_reader :status
|
|
14
14
|
|
|
15
|
+
# Response headers. Headers that appeared multiple times in the response
|
|
16
|
+
# (such as set-cookie) are flattened into a single joined string value.
|
|
17
|
+
#
|
|
15
18
|
# @return [HttpHeaders] response headers
|
|
16
19
|
attr_reader :headers
|
|
17
20
|
|
|
@@ -6,6 +6,13 @@ module PatientHttp
|
|
|
6
6
|
# Encapsulates the logic for reading async HTTP responses with size validation
|
|
7
7
|
# and building Response objects from the raw response data.
|
|
8
8
|
class ResponseReader
|
|
9
|
+
# Raised when a body read is aborted because the processor was stopped
|
|
10
|
+
# past its shutdown deadline. The shutdown sequence re-enqueues the task,
|
|
11
|
+
# so this error is handled internally and never reaches callbacks.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
class ReadAbortedError < StandardError; end
|
|
15
|
+
|
|
9
16
|
# Initialize the reader.
|
|
10
17
|
#
|
|
11
18
|
# @param processor [Processor] the processor object
|
|
@@ -24,6 +31,7 @@ module PatientHttp
|
|
|
24
31
|
# @param headers_hash [Hash] the response headers
|
|
25
32
|
# @return [String, nil] the response body or nil if no body present
|
|
26
33
|
# @raise [ResponseTooLargeError] if body exceeds max_response_size
|
|
34
|
+
# @raise [ReadAbortedError] if the processor stopped past its shutdown deadline mid-read
|
|
27
35
|
def read_body(async_response, headers_hash)
|
|
28
36
|
return nil unless async_response.body
|
|
29
37
|
|
|
@@ -58,8 +66,9 @@ module PatientHttp
|
|
|
58
66
|
# Read body chunks while checking size.
|
|
59
67
|
#
|
|
60
68
|
# @param async_response [Async::HTTP::Protocol::Response] the async HTTP response
|
|
61
|
-
# @return [String
|
|
69
|
+
# @return [String] the response body in ASCII-8BIT encoding
|
|
62
70
|
# @raise [ResponseTooLargeError] if body size exceeds max_response_size during read
|
|
71
|
+
# @raise [ReadAbortedError] if the processor stopped past its shutdown deadline mid-read
|
|
63
72
|
def read_body_chunks(async_response)
|
|
64
73
|
chunks = []
|
|
65
74
|
total_size = 0
|
|
@@ -67,9 +76,12 @@ module PatientHttp
|
|
|
67
76
|
|
|
68
77
|
begin
|
|
69
78
|
async_response.body.each do |chunk|
|
|
70
|
-
#
|
|
71
|
-
|
|
72
|
-
|
|
79
|
+
# Abort the read once the processor has passed its shutdown deadline.
|
|
80
|
+
# Reads are allowed to finish while the processor is merely stopping
|
|
81
|
+
# (the graceful shutdown window) so in-flight responses can still be
|
|
82
|
+
# delivered.
|
|
83
|
+
if @processor.stopped?
|
|
84
|
+
raise ReadAbortedError.new("Processor stopped while reading response body")
|
|
73
85
|
end
|
|
74
86
|
|
|
75
87
|
total_size += chunk.bytesize
|
|
@@ -19,6 +19,7 @@ module PatientHttp
|
|
|
19
19
|
@on_complete = on_complete
|
|
20
20
|
@on_error = on_error
|
|
21
21
|
@proxy_client = nil
|
|
22
|
+
@request_preparer = RequestPreparer.new(config)
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
# Execute the request synchronously.
|
|
@@ -30,21 +31,21 @@ module PatientHttp
|
|
|
30
31
|
begin
|
|
31
32
|
http_client = nil
|
|
32
33
|
response_data = nil
|
|
34
|
+
redirect_error = nil
|
|
33
35
|
|
|
34
36
|
loop do
|
|
35
37
|
http_client&.close
|
|
36
38
|
@proxy_client&.close
|
|
37
39
|
@proxy_client = nil
|
|
38
|
-
|
|
40
|
+
outgoing = @request_preparer.prepare(@task.request, @task.id)
|
|
41
|
+
http_client = create_http_client(outgoing.url)
|
|
39
42
|
timeout = @task.request.timeout || @config.request_timeout
|
|
40
43
|
|
|
41
44
|
response_data = Async::Task.current.with_timeout(timeout) do
|
|
42
|
-
headers =
|
|
43
|
-
headers["x-request-id"] = @task.id
|
|
44
|
-
headers["user-agent"] ||= @config.user_agent if @config.user_agent
|
|
45
|
+
headers = outgoing.headers.to_h
|
|
45
46
|
body = Protocol::HTTP::Body::Buffered.wrap([@task.request.body.to_s]) if @task.request.body
|
|
46
47
|
|
|
47
|
-
endpoint = Async::HTTP::Endpoint.parse(
|
|
48
|
+
endpoint = Async::HTTP::Endpoint.parse(outgoing.url)
|
|
48
49
|
endpoint = configure_endpoint(endpoint) if @config.connection_timeout
|
|
49
50
|
|
|
50
51
|
verb = @task.request.http_method.to_s.upcase
|
|
@@ -57,6 +58,8 @@ module PatientHttp
|
|
|
57
58
|
|
|
58
59
|
request = Protocol::HTTP::Request[verb, endpoint.path, **options]
|
|
59
60
|
async_response = http_client.call(request)
|
|
61
|
+
# Note: headers that appear multiple times (e.g. set-cookie) are
|
|
62
|
+
# flattened to a single joined string value.
|
|
60
63
|
headers_hash = async_response.headers.to_h.transform_values(&:to_s)
|
|
61
64
|
|
|
62
65
|
body_content = read_response_body(async_response, headers_hash)
|
|
@@ -71,36 +74,39 @@ module PatientHttp
|
|
|
71
74
|
# Check for redirect
|
|
72
75
|
break unless should_follow_redirect?(@task, response_data)
|
|
73
76
|
|
|
77
|
+
# Note: a `return` here would raise LocalJumpError since this block
|
|
78
|
+
# runs on a reactor fiber, so break out and handle the error below.
|
|
74
79
|
redirect_error = check_redirect_error(@task, response_data)
|
|
75
|
-
if redirect_error
|
|
76
|
-
invoke_callback(redirect_error, :error)
|
|
77
|
-
return
|
|
78
|
-
end
|
|
80
|
+
break if redirect_error
|
|
79
81
|
|
|
80
82
|
location = response_data[:headers]["location"]
|
|
81
83
|
@task = @task.redirect_task(location: location, status: response_data[:status])
|
|
82
84
|
end
|
|
83
85
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
response = Response.new(
|
|
88
|
-
status: response_data[:status],
|
|
89
|
-
headers: response_data[:headers],
|
|
90
|
-
body: response_data[:body],
|
|
91
|
-
duration: duration,
|
|
92
|
-
request_id: @task.id,
|
|
93
|
-
url: @task.request.url,
|
|
94
|
-
http_method: @task.request.http_method,
|
|
95
|
-
callback_args: @task.callback_args,
|
|
96
|
-
redirects: @task.redirects
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
if @task.raise_error_responses && !response.success?
|
|
100
|
-
http_error = HttpError.new(response)
|
|
101
|
-
invoke_callback(http_error, :error)
|
|
86
|
+
if redirect_error
|
|
87
|
+
invoke_callback(redirect_error, :error)
|
|
102
88
|
else
|
|
103
|
-
|
|
89
|
+
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
90
|
+
duration = end_time - start_time
|
|
91
|
+
|
|
92
|
+
response = Response.new(
|
|
93
|
+
status: response_data[:status],
|
|
94
|
+
headers: response_data[:headers],
|
|
95
|
+
body: response_data[:body],
|
|
96
|
+
duration: duration,
|
|
97
|
+
request_id: @task.original_id,
|
|
98
|
+
url: @task.request.url,
|
|
99
|
+
http_method: @task.request.http_method,
|
|
100
|
+
callback_args: @task.callback_args,
|
|
101
|
+
redirects: @task.redirects
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if @task.raise_error_responses && !response.success?
|
|
105
|
+
http_error = HttpError.new(response)
|
|
106
|
+
invoke_callback(http_error, :error)
|
|
107
|
+
else
|
|
108
|
+
invoke_callback(response, :response)
|
|
109
|
+
end
|
|
104
110
|
end
|
|
105
111
|
rescue => e
|
|
106
112
|
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
@@ -125,18 +131,12 @@ module PatientHttp
|
|
|
125
131
|
|
|
126
132
|
private
|
|
127
133
|
|
|
128
|
-
# Resolve the current task's request URL, appending any secret query params.
|
|
129
|
-
#
|
|
130
|
-
# @return [String] the resolved request URL
|
|
131
|
-
def request_url
|
|
132
|
-
@config.secret_manager.resolve_url(@task.request.url, @task.request.secret_params)
|
|
133
|
-
end
|
|
134
|
-
|
|
135
134
|
# Create HTTP client with config settings (retries, proxy, connection timeout).
|
|
136
135
|
#
|
|
136
|
+
# @param url [String] the resolved request URL
|
|
137
137
|
# @return [Protocol::HTTP::AcceptEncoding] wrapped HTTP client
|
|
138
|
-
def create_http_client
|
|
139
|
-
endpoint = Async::HTTP::Endpoint.parse(
|
|
138
|
+
def create_http_client(url)
|
|
139
|
+
endpoint = Async::HTTP::Endpoint.parse(url)
|
|
140
140
|
endpoint = configure_endpoint(endpoint) if @config.connection_timeout
|
|
141
141
|
|
|
142
142
|
client = if @config.proxy_url
|
|
@@ -191,15 +191,23 @@ module PatientHttp
|
|
|
191
191
|
|
|
192
192
|
chunks = []
|
|
193
193
|
total_size = 0
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
194
|
+
finished = false
|
|
195
|
+
|
|
196
|
+
begin
|
|
197
|
+
async_response.body.each do |chunk|
|
|
198
|
+
total_size += chunk.bytesize
|
|
199
|
+
if total_size > @config.max_response_size
|
|
200
|
+
raise ResponseTooLargeError.new(
|
|
201
|
+
"Response body size exceeded maximum allowed size (#{@config.max_response_size} bytes)"
|
|
202
|
+
)
|
|
203
|
+
end
|
|
204
|
+
chunks << chunk
|
|
201
205
|
end
|
|
202
|
-
|
|
206
|
+
|
|
207
|
+
finished = true
|
|
208
|
+
ensure
|
|
209
|
+
# Close the body if the read was interrupted so the connection is released
|
|
210
|
+
async_response.body.close unless finished
|
|
203
211
|
end
|
|
204
212
|
|
|
205
213
|
body = chunks.join.force_encoding(Encoding::ASCII_8BIT)
|
data/lib/patient_http.rb
CHANGED
|
@@ -52,6 +52,7 @@ module PatientHttp
|
|
|
52
52
|
autoload :HttpError, File.join(__dir__, "patient_http/http_error")
|
|
53
53
|
autoload :HttpHeaders, File.join(__dir__, "patient_http/http_headers")
|
|
54
54
|
autoload :LifecycleManager, File.join(__dir__, "patient_http/lifecycle_manager")
|
|
55
|
+
autoload :OutgoingRequest, File.join(__dir__, "patient_http/outgoing_request")
|
|
55
56
|
autoload :Payload, File.join(__dir__, "patient_http/payload")
|
|
56
57
|
autoload :PayloadStore, File.join(__dir__, "patient_http/payload_store")
|
|
57
58
|
autoload :Processor, File.join(__dir__, "patient_http/processor")
|
|
@@ -62,6 +63,7 @@ module PatientHttp
|
|
|
62
63
|
autoload :Request, File.join(__dir__, "patient_http/request")
|
|
63
64
|
autoload :RequestError, File.join(__dir__, "patient_http/request_error")
|
|
64
65
|
autoload :RequestHelper, File.join(__dir__, "patient_http/request_helper")
|
|
66
|
+
autoload :RequestPreparer, File.join(__dir__, "patient_http/request_preparer")
|
|
65
67
|
autoload :RequestTask, File.join(__dir__, "patient_http/request_task")
|
|
66
68
|
autoload :RequestTemplate, File.join(__dir__, "patient_http/request_template")
|
|
67
69
|
autoload :Response, File.join(__dir__, "patient_http/response")
|
|
@@ -238,6 +240,8 @@ module PatientHttp
|
|
|
238
240
|
# @param raise_error_responses [Boolean, nil] when true, non-success responses are
|
|
239
241
|
# reported as errors
|
|
240
242
|
# @param callback_args [Hash, nil] JSON-compatible callback arguments
|
|
243
|
+
# @param preprocessors [String, Symbol, Array<String, Symbol>, nil] names of preprocessors
|
|
244
|
+
# registered on the configuration to apply to the request when it is sent
|
|
241
245
|
# @return [Object] return value from the registered request handler
|
|
242
246
|
def request(
|
|
243
247
|
method,
|
|
@@ -249,9 +253,19 @@ module PatientHttp
|
|
|
249
253
|
params: nil,
|
|
250
254
|
timeout: nil,
|
|
251
255
|
raise_error_responses: nil,
|
|
252
|
-
callback_args: nil
|
|
256
|
+
callback_args: nil,
|
|
257
|
+
preprocessors: nil
|
|
253
258
|
)
|
|
254
|
-
request = Request.new(
|
|
259
|
+
request = Request.new(
|
|
260
|
+
method,
|
|
261
|
+
url,
|
|
262
|
+
body: body,
|
|
263
|
+
json: json,
|
|
264
|
+
headers: headers,
|
|
265
|
+
params: params,
|
|
266
|
+
timeout: timeout,
|
|
267
|
+
preprocessors: preprocessors
|
|
268
|
+
)
|
|
255
269
|
execute(
|
|
256
270
|
request: request,
|
|
257
271
|
callback: callback,
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: patient_http
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brian Durand
|
|
@@ -110,6 +110,7 @@ files:
|
|
|
110
110
|
- lib/patient_http/http_error.rb
|
|
111
111
|
- lib/patient_http/http_headers.rb
|
|
112
112
|
- lib/patient_http/lifecycle_manager.rb
|
|
113
|
+
- lib/patient_http/outgoing_request.rb
|
|
113
114
|
- lib/patient_http/payload.rb
|
|
114
115
|
- lib/patient_http/payload_store.rb
|
|
115
116
|
- lib/patient_http/payload_store/active_record_store.rb
|
|
@@ -125,6 +126,7 @@ files:
|
|
|
125
126
|
- lib/patient_http/request.rb
|
|
126
127
|
- lib/patient_http/request_error.rb
|
|
127
128
|
- lib/patient_http/request_helper.rb
|
|
129
|
+
- lib/patient_http/request_preparer.rb
|
|
128
130
|
- lib/patient_http/request_task.rb
|
|
129
131
|
- lib/patient_http/request_template.rb
|
|
130
132
|
- lib/patient_http/response.rb
|