patient_http 1.6.1 → 1.7.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/ARCHITECTURE.md +8 -7
- data/CHANGELOG.md +29 -0
- data/README.md +539 -515
- data/VERSION +1 -1
- data/lib/patient_http/callback_args.rb +53 -48
- data/lib/patient_http/callback_validator.rb +11 -7
- data/lib/patient_http/class_helper.rb +6 -7
- data/lib/patient_http/client.rb +28 -22
- data/lib/patient_http/client_pool.rb +134 -39
- data/lib/patient_http/completion_executor.rb +20 -20
- data/lib/patient_http/configuration.rb +367 -119
- data/lib/patient_http/connection_endpoint.rb +150 -0
- data/lib/patient_http/encryptor.rb +28 -18
- data/lib/patient_http/error.rb +24 -18
- data/lib/patient_http/external_storage.rb +42 -38
- data/lib/patient_http/http_error.rb +30 -26
- data/lib/patient_http/http_headers.rb +35 -29
- data/lib/patient_http/immediate_retries.rb +98 -0
- data/lib/patient_http/inline_task_handler.rb +15 -10
- data/lib/patient_http/lifecycle_manager.rb +39 -40
- data/lib/patient_http/outgoing_request.rb +25 -23
- data/lib/patient_http/payload.rb +28 -26
- data/lib/patient_http/payload_store/active_record_store.rb +31 -34
- data/lib/patient_http/payload_store/base.rb +42 -46
- data/lib/patient_http/payload_store/file_store.rb +22 -26
- data/lib/patient_http/payload_store/redis_store.rb +28 -34
- data/lib/patient_http/payload_store/s3_store.rb +25 -28
- data/lib/patient_http/payload_store.rb +2 -0
- data/lib/patient_http/processor.rb +111 -79
- data/lib/patient_http/processor_observer.rb +65 -59
- data/lib/patient_http/rails/engine.rb +13 -8
- data/lib/patient_http/redirect_error.rb +50 -41
- data/lib/patient_http/redirect_helper.rb +38 -38
- data/lib/patient_http/request.rb +70 -46
- data/lib/patient_http/request_error.rb +47 -42
- data/lib/patient_http/request_helper.rb +142 -119
- data/lib/patient_http/request_preparer.rb +13 -10
- data/lib/patient_http/request_task.rb +113 -84
- data/lib/patient_http/request_template.rb +87 -64
- data/lib/patient_http/response.rb +58 -52
- data/lib/patient_http/response_reader.rb +66 -65
- data/lib/patient_http/secret_manager.rb +34 -30
- data/lib/patient_http/secret_reference.rb +33 -26
- data/lib/patient_http/synchronous_executor.rb +67 -95
- data/lib/patient_http/task_handler.rb +23 -19
- data/lib/patient_http/time_helper.rb +8 -8
- data/lib/patient_http.rb +311 -186
- data/patient_http.gemspec +3 -2
- metadata +21 -5
data/lib/patient_http/request.rb
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PatientHttp
|
|
4
|
-
#
|
|
4
|
+
# An HTTP request that a processor runs asynchronously.
|
|
5
5
|
#
|
|
6
|
-
# @example
|
|
6
|
+
# @example Create a GET request
|
|
7
7
|
# request = PatientHttp::Request.new(:get, "https://api.example.com/users/123")
|
|
8
8
|
#
|
|
9
|
-
# @example
|
|
9
|
+
# @example Create a POST request with a JSON body
|
|
10
10
|
# request = PatientHttp::Request.new(
|
|
11
11
|
# :post,
|
|
12
12
|
# "https://api.example.com/users",
|
|
@@ -16,53 +16,61 @@ module PatientHttp
|
|
|
16
16
|
UNDEFINED = Object.new.freeze
|
|
17
17
|
private_constant :UNDEFINED
|
|
18
18
|
|
|
19
|
-
#
|
|
19
|
+
# The supported HTTP methods.
|
|
20
20
|
VALID_METHODS = %i[get head post put patch delete query].freeze
|
|
21
21
|
|
|
22
|
-
# HTTP methods that
|
|
22
|
+
# The HTTP methods that can't have a request body.
|
|
23
23
|
BODYLESS_METHODS = %i[get head delete].freeze
|
|
24
24
|
|
|
25
|
-
#
|
|
25
|
+
# The HTTP methods that can be sent again without changing the result. As in
|
|
26
|
+
# RFC 9110, POST and PATCH aren't included.
|
|
27
|
+
IDEMPOTENT_METHODS = [:get, :head, :put, :delete, :query].freeze
|
|
28
|
+
|
|
29
|
+
# @return [Symbol] The HTTP method: `:get`, `:head`, `:post`, `:put`, `:patch`,
|
|
30
|
+
# `:delete`, or `:query`.
|
|
26
31
|
attr_reader :http_method
|
|
27
32
|
|
|
28
|
-
# @return [String] The request URL
|
|
33
|
+
# @return [String] The request URL.
|
|
29
34
|
attr_reader :url
|
|
30
35
|
|
|
31
|
-
# @return [HttpHeaders]
|
|
36
|
+
# @return [HttpHeaders] The request headers.
|
|
32
37
|
attr_reader :headers
|
|
33
38
|
|
|
34
|
-
# @return [Numeric, nil]
|
|
39
|
+
# @return [Numeric, nil] The timeout in seconds for the full request.
|
|
35
40
|
attr_reader :timeout
|
|
36
41
|
|
|
37
|
-
# @return [Integer, nil]
|
|
42
|
+
# @return [Integer, nil] The maximum number of redirects to follow. If `0`,
|
|
43
|
+
# redirects aren't followed. If `nil`, the configuration value applies.
|
|
38
44
|
attr_reader :max_redirects
|
|
39
45
|
|
|
40
|
-
# @return [Boolean, nil] Whether a redirect that
|
|
41
|
-
#
|
|
46
|
+
# @return [Boolean, nil] Whether to follow a redirect that changes the HTTP
|
|
47
|
+
# method, such as POST to GET on a 302. If `nil`, the configuration value
|
|
48
|
+
# applies.
|
|
42
49
|
attr_reader :follow_method_changing_redirects
|
|
43
50
|
|
|
44
|
-
# @return [Array<String>]
|
|
45
|
-
# in addition to
|
|
51
|
+
# @return [Array<String>] The lowercase names of headers to remove from
|
|
52
|
+
# redirected requests, in addition to the names set in the {Configuration}.
|
|
46
53
|
attr_reader :redirect_strip_headers
|
|
47
54
|
|
|
48
|
-
# @return [Hash{String, Symbol => SecretReference}]
|
|
49
|
-
# secret references
|
|
55
|
+
# @return [Hash{String, Symbol => SecretReference}] The query parameters whose
|
|
56
|
+
# values are secret references. They aren't in the serialized URL. The
|
|
57
|
+
# processor resolves them when it sends the request.
|
|
50
58
|
attr_reader :secret_params
|
|
51
59
|
|
|
52
|
-
# @return [Array<String>]
|
|
53
|
-
#
|
|
60
|
+
# @return [Array<String>] The names of the registered preprocessors that run
|
|
61
|
+
# on the request before it's sent.
|
|
54
62
|
attr_reader :preprocessors
|
|
55
63
|
|
|
56
|
-
# @return [String, nil]
|
|
57
|
-
# Integrations use this to
|
|
58
|
-
#
|
|
64
|
+
# @return [String, nil] The name of the processor that runs the request.
|
|
65
|
+
# Integrations use this value to send the request to a named processor. If
|
|
66
|
+
# `nil`, the default processor runs the request.
|
|
59
67
|
attr_reader :processor
|
|
60
68
|
|
|
61
69
|
class << self
|
|
62
|
-
#
|
|
70
|
+
# Creates a request from its serialized form.
|
|
63
71
|
#
|
|
64
|
-
# @param hash [Hash] hash
|
|
65
|
-
# @return [Request]
|
|
72
|
+
# @param hash [Hash] The hash from {#as_json}.
|
|
73
|
+
# @return [Request] The request.
|
|
66
74
|
def load(hash)
|
|
67
75
|
new(
|
|
68
76
|
hash["http_method"].to_sym,
|
|
@@ -98,26 +106,34 @@ module PatientHttp
|
|
|
98
106
|
end
|
|
99
107
|
end
|
|
100
108
|
|
|
101
|
-
#
|
|
109
|
+
# Creates a request.
|
|
102
110
|
#
|
|
103
|
-
# @param http_method [Symbol, String] HTTP method
|
|
111
|
+
# @param http_method [Symbol, String] The HTTP method: `:get`, `:head`, `:post`,
|
|
112
|
+
# `:put`, `:patch`, `:delete`, or `:query`.
|
|
104
113
|
# @param url [String, URI::Generic] The request URL.
|
|
105
|
-
# @param headers [Hash, HttpHeaders]
|
|
106
|
-
# @param body [String, nil]
|
|
107
|
-
#
|
|
108
|
-
# @param
|
|
109
|
-
#
|
|
110
|
-
# @param
|
|
111
|
-
# @param
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
#
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
#
|
|
119
|
-
#
|
|
120
|
-
#
|
|
114
|
+
# @param headers [Hash, HttpHeaders] The request headers.
|
|
115
|
+
# @param body [String, nil] The request body. GET, HEAD, and DELETE requests
|
|
116
|
+
# can't have a body.
|
|
117
|
+
# @param json [Object, nil] An object to send as a JSON body. Can't be combined
|
|
118
|
+
# with `body`.
|
|
119
|
+
# @param params [Hash, nil] The query parameters to add to the URL.
|
|
120
|
+
# @param timeout [Numeric, nil] The timeout in seconds for the full request.
|
|
121
|
+
# @param max_redirects [Integer, nil] The maximum number of redirects to
|
|
122
|
+
# follow. If `0`, redirects aren't followed. If `nil`, the configuration
|
|
123
|
+
# value applies.
|
|
124
|
+
# @param follow_method_changing_redirects [Boolean, nil] Whether to follow a
|
|
125
|
+
# redirect that changes the HTTP method. If `false`, the redirect response is
|
|
126
|
+
# the result. If `nil`, the configuration value applies.
|
|
127
|
+
# @param redirect_strip_headers [String, Array<String>, nil] The names of headers
|
|
128
|
+
# to remove from redirected requests, in addition to the names set in the
|
|
129
|
+
# {Configuration}. Names are case insensitive.
|
|
130
|
+
# @param preprocessors [String, Symbol, Array<String, Symbol>, nil] The names of
|
|
131
|
+
# the registered preprocessors that run on the request before it's sent.
|
|
132
|
+
# @param processor [String, Symbol, nil] The name of the processor that runs
|
|
133
|
+
# the request. Integrations use this value to send the request to a named
|
|
134
|
+
# processor.
|
|
135
|
+
# @raise [ArgumentError] If the HTTP method, URL, or body isn't valid, or if
|
|
136
|
+
# both `body` and `json` are given.
|
|
121
137
|
def initialize(
|
|
122
138
|
http_method,
|
|
123
139
|
url,
|
|
@@ -165,17 +181,25 @@ module PatientHttp
|
|
|
165
181
|
@body = UNDEFINED
|
|
166
182
|
end
|
|
167
183
|
|
|
168
|
-
# Returns the request body
|
|
184
|
+
# Returns the request body. The body is decoded on first access.
|
|
169
185
|
#
|
|
170
|
-
# @return [String, nil] The
|
|
186
|
+
# @return [String, nil] The request body, or `nil` if the request has no body.
|
|
171
187
|
def body
|
|
172
188
|
@body = @payload&.value if @body.equal?(UNDEFINED)
|
|
173
189
|
@body
|
|
174
190
|
end
|
|
175
191
|
|
|
176
|
-
#
|
|
192
|
+
# Returns whether the request can be sent again without changing the result.
|
|
193
|
+
# The HTTP method determines the value.
|
|
194
|
+
#
|
|
195
|
+
# @return [Boolean] `true` if the HTTP method is idempotent.
|
|
196
|
+
def idempotent?
|
|
197
|
+
IDEMPOTENT_METHODS.include?(@http_method)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Returns the request as a JSON-compatible hash.
|
|
177
201
|
#
|
|
178
|
-
# @return [Hash]
|
|
202
|
+
# @return [Hash] The serialized request.
|
|
179
203
|
def as_json
|
|
180
204
|
hash = {
|
|
181
205
|
"http_method" => @http_method.to_s,
|
|
@@ -1,36 +1,37 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PatientHttp
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
4
|
+
# The error for an exception that occurs while a request runs, such as a
|
|
5
|
+
# timeout, a connection failure, or an SSL error. HTTP error responses (4xx
|
|
6
|
+
# and 5xx) use {HttpError} instead.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
8
|
+
# The error can be serialized, so a job system can pass it to the `on_error`
|
|
9
|
+
# callback in another process.
|
|
9
10
|
class RequestError < Error
|
|
10
|
-
#
|
|
11
|
+
# The valid error types.
|
|
11
12
|
ERROR_TYPES = [:timeout, :connection, :ssl, :response_too_large, :unknown].freeze
|
|
12
13
|
|
|
13
|
-
# @return [String]
|
|
14
|
+
# @return [String] The request URL.
|
|
14
15
|
attr_reader :url
|
|
15
16
|
|
|
16
|
-
# @return [Symbol] HTTP method
|
|
17
|
+
# @return [Symbol] The HTTP method.
|
|
17
18
|
attr_reader :http_method
|
|
18
19
|
|
|
19
|
-
# @return [Float]
|
|
20
|
+
# @return [Float] The request duration in seconds.
|
|
20
21
|
attr_reader :duration
|
|
21
22
|
|
|
22
|
-
# @return [String]
|
|
23
|
+
# @return [String] The unique request ID.
|
|
23
24
|
attr_reader :request_id
|
|
24
25
|
|
|
25
|
-
# @return [Symbol]
|
|
26
|
-
#
|
|
26
|
+
# @return [Symbol] The error category, one of {ERROR_TYPES}. For example,
|
|
27
|
+
# `:connection` includes I/O and socket errors.
|
|
27
28
|
attr_reader :error_type
|
|
28
29
|
|
|
29
30
|
class << self
|
|
30
|
-
#
|
|
31
|
+
# Creates an error from its serialized form.
|
|
31
32
|
#
|
|
32
|
-
# @param hash [Hash] hash
|
|
33
|
-
# @return [RequestError]
|
|
33
|
+
# @param hash [Hash] The hash from {#as_json}.
|
|
34
|
+
# @return [RequestError] The error.
|
|
34
35
|
def load(hash)
|
|
35
36
|
new(
|
|
36
37
|
class_name: hash["class_name"],
|
|
@@ -45,15 +46,15 @@ module PatientHttp
|
|
|
45
46
|
)
|
|
46
47
|
end
|
|
47
48
|
|
|
48
|
-
#
|
|
49
|
+
# Creates an error from an exception.
|
|
49
50
|
#
|
|
50
|
-
# @param exception [Exception]
|
|
51
|
-
# @param duration [Float] request duration in seconds
|
|
52
|
-
# @param request_id [String]
|
|
53
|
-
# @param url [String]
|
|
54
|
-
# @param http_method [Symbol, String]
|
|
55
|
-
# @param callback_args [Hash, nil] callback arguments
|
|
56
|
-
# @return [RequestError]
|
|
51
|
+
# @param exception [Exception] The exception that the request raised.
|
|
52
|
+
# @param duration [Float] The request duration in seconds.
|
|
53
|
+
# @param request_id [String] The request ID.
|
|
54
|
+
# @param url [String] The request URL.
|
|
55
|
+
# @param http_method [Symbol, String] The HTTP method.
|
|
56
|
+
# @param callback_args [Hash, nil] The callback arguments, with string keys.
|
|
57
|
+
# @return [RequestError] The error.
|
|
57
58
|
def from_exception(exception, duration:, request_id:, url:, http_method:, callback_args: nil)
|
|
58
59
|
type = error_type(exception)
|
|
59
60
|
|
|
@@ -70,13 +71,16 @@ module PatientHttp
|
|
|
70
71
|
)
|
|
71
72
|
end
|
|
72
73
|
|
|
73
|
-
#
|
|
74
|
+
# Returns the error type for an exception.
|
|
74
75
|
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
76
|
+
# `IO::TimeoutError` is a subclass of `IOError`, but its type is `:timeout`,
|
|
77
|
+
# not `:connection`.
|
|
78
|
+
#
|
|
79
|
+
# @param exception [Exception] The exception.
|
|
80
|
+
# @return [Symbol] The error type, one of {ERROR_TYPES}.
|
|
77
81
|
def error_type(exception)
|
|
78
82
|
case exception
|
|
79
|
-
in Async::TimeoutError
|
|
83
|
+
in Async::TimeoutError | IO::TimeoutError
|
|
80
84
|
:timeout
|
|
81
85
|
in OpenSSL::SSL::SSLError
|
|
82
86
|
:ssl
|
|
@@ -92,17 +96,17 @@ module PatientHttp
|
|
|
92
96
|
end
|
|
93
97
|
end
|
|
94
98
|
|
|
95
|
-
#
|
|
99
|
+
# Creates an error.
|
|
96
100
|
#
|
|
97
|
-
# @param class_name [String]
|
|
98
|
-
# @param message [String]
|
|
99
|
-
# @param backtrace [Array<String>]
|
|
100
|
-
# @param error_type [Symbol]
|
|
101
|
-
# @param duration [Float]
|
|
102
|
-
# @param request_id [String]
|
|
103
|
-
# @param url [String]
|
|
104
|
-
# @param http_method [Symbol, String] HTTP method
|
|
105
|
-
# @param callback_args [Hash, nil] callback arguments
|
|
101
|
+
# @param class_name [String] The name of the exception class.
|
|
102
|
+
# @param message [String] The exception message.
|
|
103
|
+
# @param backtrace [Array<String>] The exception backtrace.
|
|
104
|
+
# @param error_type [Symbol] The error type, one of {ERROR_TYPES}.
|
|
105
|
+
# @param duration [Float] The request duration in seconds.
|
|
106
|
+
# @param request_id [String] The unique request ID.
|
|
107
|
+
# @param url [String] The request URL.
|
|
108
|
+
# @param http_method [Symbol, String] The HTTP method.
|
|
109
|
+
# @param callback_args [Hash, nil] The callback arguments, with string keys.
|
|
106
110
|
def initialize(class_name:, message:, backtrace:, error_type:, duration:, request_id:, url:, http_method:,
|
|
107
111
|
callback_args: nil)
|
|
108
112
|
super(message)
|
|
@@ -116,9 +120,9 @@ module PatientHttp
|
|
|
116
120
|
@callback_args_data = callback_args || {}
|
|
117
121
|
end
|
|
118
122
|
|
|
119
|
-
#
|
|
123
|
+
# Returns the error as a JSON-compatible hash.
|
|
120
124
|
#
|
|
121
|
-
# @return [Hash]
|
|
125
|
+
# @return [Hash] The serialized error.
|
|
122
126
|
def as_json
|
|
123
127
|
{
|
|
124
128
|
"class_name" => @class_name,
|
|
@@ -133,16 +137,17 @@ module PatientHttp
|
|
|
133
137
|
}
|
|
134
138
|
end
|
|
135
139
|
|
|
136
|
-
#
|
|
140
|
+
# Returns the class of the exception that caused the error.
|
|
137
141
|
#
|
|
138
|
-
# @return [Class, nil]
|
|
142
|
+
# @return [Class, nil] The exception class, or `nil` if the class isn't
|
|
143
|
+
# defined in this process.
|
|
139
144
|
def error_class
|
|
140
145
|
ClassHelper.resolve_class_name(@class_name)
|
|
141
146
|
end
|
|
142
147
|
|
|
143
|
-
# Returns the callback arguments
|
|
148
|
+
# Returns the callback arguments that were passed with the request.
|
|
144
149
|
#
|
|
145
|
-
# @return [CallbackArgs]
|
|
150
|
+
# @return [CallbackArgs] The callback arguments.
|
|
146
151
|
def callback_args
|
|
147
152
|
@callback_args ||= CallbackArgs.load(@callback_args_data)
|
|
148
153
|
end
|