patient_http 1.1.2 → 1.3.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 +31 -0
- data/README.md +116 -3
- data/VERSION +1 -1
- data/lib/patient_http/client.rb +7 -10
- data/lib/patient_http/configuration.rb +56 -1
- data/lib/patient_http/http_headers.rb +7 -0
- data/lib/patient_http/inline_task_handler.rb +30 -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 +299 -113
- 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_helper.rb +16 -6
- 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 +182 -2
- metadata +5 -2
data/lib/patient_http/request.rb
CHANGED
|
@@ -38,6 +38,10 @@ module PatientHttp
|
|
|
38
38
|
# secret references, kept out of the serialized URL and resolved at send time
|
|
39
39
|
attr_reader :secret_params
|
|
40
40
|
|
|
41
|
+
# @return [Array<String>] Names of preprocessors registered on the configuration
|
|
42
|
+
# to apply to the request when it is sent
|
|
43
|
+
attr_reader :preprocessors
|
|
44
|
+
|
|
41
45
|
class << self
|
|
42
46
|
# Reconstruct a Request from a hash
|
|
43
47
|
#
|
|
@@ -51,7 +55,8 @@ module PatientHttp
|
|
|
51
55
|
body: Payload.load(hash["body"])&.value,
|
|
52
56
|
params: load_secret_params(hash["secret_params"]),
|
|
53
57
|
timeout: hash["timeout"],
|
|
54
|
-
max_redirects: hash["max_redirects"]
|
|
58
|
+
max_redirects: hash["max_redirects"],
|
|
59
|
+
preprocessors: hash["preprocessors"]
|
|
55
60
|
)
|
|
56
61
|
end
|
|
57
62
|
|
|
@@ -84,6 +89,8 @@ module PatientHttp
|
|
|
84
89
|
# @param params [Hash, nil] Query parameters to append to the URL.
|
|
85
90
|
# @param timeout [Numeric, nil] Overall timeout in seconds.
|
|
86
91
|
# @param max_redirects [Integer, nil] Maximum redirects to follow (nil uses config, 0 disables).
|
|
92
|
+
# @param preprocessors [String, Symbol, Array<String, Symbol>, nil] Names of preprocessors
|
|
93
|
+
# registered on the configuration to apply to the request when it is sent.
|
|
87
94
|
def initialize(
|
|
88
95
|
http_method,
|
|
89
96
|
url,
|
|
@@ -92,7 +99,8 @@ module PatientHttp
|
|
|
92
99
|
json: nil,
|
|
93
100
|
params: nil,
|
|
94
101
|
timeout: nil,
|
|
95
|
-
max_redirects: nil
|
|
102
|
+
max_redirects: nil,
|
|
103
|
+
preprocessors: nil
|
|
96
104
|
)
|
|
97
105
|
@http_method = http_method.is_a?(String) ? http_method.downcase.to_sym : http_method
|
|
98
106
|
|
|
@@ -102,10 +110,13 @@ module PatientHttp
|
|
|
102
110
|
|
|
103
111
|
@secret_params = {}
|
|
104
112
|
@url = normalized_url(url, params)
|
|
105
|
-
|
|
113
|
+
# Copy the headers so the request does not share mutable state with the
|
|
114
|
+
# caller (or with another request when following redirects).
|
|
115
|
+
@headers = headers.is_a?(HttpHeaders) ? headers.dup : HttpHeaders.new(headers)
|
|
106
116
|
@body = (body == "") ? nil : body
|
|
107
117
|
@timeout = timeout
|
|
108
118
|
@max_redirects = max_redirects
|
|
119
|
+
@preprocessors = normalized_preprocessors(preprocessors)
|
|
109
120
|
|
|
110
121
|
if json
|
|
111
122
|
raise ArgumentError.new("Cannot provide both body and json") if @body
|
|
@@ -146,6 +157,8 @@ module PatientHttp
|
|
|
146
157
|
hash["secret_params"] = @secret_params.transform_values(&:as_json)
|
|
147
158
|
end
|
|
148
159
|
|
|
160
|
+
hash["preprocessors"] = @preprocessors if @preprocessors.any?
|
|
161
|
+
|
|
149
162
|
hash
|
|
150
163
|
end
|
|
151
164
|
|
|
@@ -158,6 +171,16 @@ module PatientHttp
|
|
|
158
171
|
end
|
|
159
172
|
end
|
|
160
173
|
|
|
174
|
+
# Normalize preprocessor names to a frozen array of strings.
|
|
175
|
+
def normalized_preprocessors(preprocessors)
|
|
176
|
+
names = Array(preprocessors).map(&:to_s)
|
|
177
|
+
if names.any?(&:empty?)
|
|
178
|
+
raise ArgumentError.new("preprocessor names cannot be empty")
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
names.freeze
|
|
182
|
+
end
|
|
183
|
+
|
|
161
184
|
def normalized_url(url, params)
|
|
162
185
|
uri = url.is_a?(URI::Generic) ? url.dup : URI(url.to_s)
|
|
163
186
|
return uri.to_s unless params&.any?
|
|
@@ -80,7 +80,7 @@ module PatientHttp
|
|
|
80
80
|
:timeout
|
|
81
81
|
in OpenSSL::SSL::SSLError
|
|
82
82
|
:ssl
|
|
83
|
-
in Errno::ECONNREFUSED | Errno::ECONNRESET | Errno::ECONNABORTED | Errno::EHOSTUNREACH | Errno::EPIPE | SocketError | IOError
|
|
83
|
+
in Errno::ECONNREFUSED | Errno::ECONNRESET | Errno::ECONNABORTED | Errno::EHOSTUNREACH | Errno::ETIMEDOUT | Errno::EPIPE | SocketError | IOError
|
|
84
84
|
:connection
|
|
85
85
|
else
|
|
86
86
|
if exception.is_a?(PatientHttp::ResponseTooLargeError)
|
|
@@ -114,13 +114,16 @@ module PatientHttp
|
|
|
114
114
|
# @param headers [Hash] default headers for requests
|
|
115
115
|
# @param params [Hash, nil] default query parameters for requests
|
|
116
116
|
# @param timeout [Float] default timeout in seconds
|
|
117
|
+
# @param preprocessors [String, Symbol, Array<String, Symbol>, nil] default names of
|
|
118
|
+
# preprocessors registered on the configuration to apply to requests
|
|
117
119
|
# @return [void]
|
|
118
|
-
def request_template(base_url: nil, headers: {}, params: nil, timeout: 30)
|
|
120
|
+
def request_template(base_url: nil, headers: {}, params: nil, timeout: 30, preprocessors: nil)
|
|
119
121
|
@patient_http_request_template = RequestTemplate.new(
|
|
120
122
|
base_url: base_url,
|
|
121
123
|
headers: headers,
|
|
122
124
|
params: params,
|
|
123
|
-
timeout: timeout
|
|
125
|
+
timeout: timeout,
|
|
126
|
+
preprocessors: preprocessors
|
|
124
127
|
)
|
|
125
128
|
end
|
|
126
129
|
|
|
@@ -140,6 +143,8 @@ module PatientHttp
|
|
|
140
143
|
# @param raise_error_responses [Boolean, nil] when true, non-success responses are
|
|
141
144
|
# reported as errors
|
|
142
145
|
# @param callback_args [Hash, nil] JSON-compatible callback arguments
|
|
146
|
+
# @param preprocessors [String, Symbol, Array<String, Symbol>, nil] names of preprocessors
|
|
147
|
+
# registered on the configuration to apply to the request when it is sent
|
|
143
148
|
# @return [Object] return value from the registered request handler
|
|
144
149
|
def async_request(
|
|
145
150
|
method,
|
|
@@ -151,10 +156,11 @@ module PatientHttp
|
|
|
151
156
|
params: nil,
|
|
152
157
|
timeout: nil,
|
|
153
158
|
raise_error_responses: nil,
|
|
154
|
-
callback_args: nil
|
|
159
|
+
callback_args: nil,
|
|
160
|
+
preprocessors: nil
|
|
155
161
|
)
|
|
156
162
|
template = async_request_template
|
|
157
|
-
kwargs = {body: body, json: json, headers: headers, params: params, timeout: timeout}
|
|
163
|
+
kwargs = {body: body, json: json, headers: headers, params: params, timeout: timeout, preprocessors: preprocessors}
|
|
158
164
|
request = if template
|
|
159
165
|
template.request(method, url, **kwargs)
|
|
160
166
|
else
|
|
@@ -198,6 +204,8 @@ module PatientHttp
|
|
|
198
204
|
# @param raise_error_responses [Boolean, nil] when true, non-success responses are
|
|
199
205
|
# reported as errors
|
|
200
206
|
# @param callback_args [Hash, nil] JSON-compatible callback arguments
|
|
207
|
+
# @param preprocessors [String, Symbol, Array<String, Symbol>, nil] names of preprocessors
|
|
208
|
+
# registered on the configuration to apply to the request when it is sent
|
|
201
209
|
# @return [Object] return value from the registered request handler
|
|
202
210
|
def async_request(
|
|
203
211
|
method,
|
|
@@ -209,7 +217,8 @@ module PatientHttp
|
|
|
209
217
|
params: nil,
|
|
210
218
|
timeout: nil,
|
|
211
219
|
raise_error_responses: nil,
|
|
212
|
-
callback_args: nil
|
|
220
|
+
callback_args: nil,
|
|
221
|
+
preprocessors: nil
|
|
213
222
|
)
|
|
214
223
|
self.class.async_request(
|
|
215
224
|
method,
|
|
@@ -221,7 +230,8 @@ module PatientHttp
|
|
|
221
230
|
params: params,
|
|
222
231
|
timeout: timeout,
|
|
223
232
|
raise_error_responses: raise_error_responses,
|
|
224
|
-
callback_args: callback_args
|
|
233
|
+
callback_args: callback_args,
|
|
234
|
+
preprocessors: preprocessors
|
|
225
235
|
)
|
|
226
236
|
end
|
|
227
237
|
|
|
@@ -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)
|