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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +8 -7
  3. data/CHANGELOG.md +29 -0
  4. data/README.md +539 -515
  5. data/VERSION +1 -1
  6. data/lib/patient_http/callback_args.rb +53 -48
  7. data/lib/patient_http/callback_validator.rb +11 -7
  8. data/lib/patient_http/class_helper.rb +6 -7
  9. data/lib/patient_http/client.rb +28 -22
  10. data/lib/patient_http/client_pool.rb +134 -39
  11. data/lib/patient_http/completion_executor.rb +20 -20
  12. data/lib/patient_http/configuration.rb +367 -119
  13. data/lib/patient_http/connection_endpoint.rb +150 -0
  14. data/lib/patient_http/encryptor.rb +28 -18
  15. data/lib/patient_http/error.rb +24 -18
  16. data/lib/patient_http/external_storage.rb +42 -38
  17. data/lib/patient_http/http_error.rb +30 -26
  18. data/lib/patient_http/http_headers.rb +35 -29
  19. data/lib/patient_http/immediate_retries.rb +98 -0
  20. data/lib/patient_http/inline_task_handler.rb +15 -10
  21. data/lib/patient_http/lifecycle_manager.rb +39 -40
  22. data/lib/patient_http/outgoing_request.rb +25 -23
  23. data/lib/patient_http/payload.rb +28 -26
  24. data/lib/patient_http/payload_store/active_record_store.rb +31 -34
  25. data/lib/patient_http/payload_store/base.rb +42 -46
  26. data/lib/patient_http/payload_store/file_store.rb +22 -26
  27. data/lib/patient_http/payload_store/redis_store.rb +28 -34
  28. data/lib/patient_http/payload_store/s3_store.rb +25 -28
  29. data/lib/patient_http/payload_store.rb +2 -0
  30. data/lib/patient_http/processor.rb +111 -79
  31. data/lib/patient_http/processor_observer.rb +65 -59
  32. data/lib/patient_http/rails/engine.rb +13 -8
  33. data/lib/patient_http/redirect_error.rb +50 -41
  34. data/lib/patient_http/redirect_helper.rb +38 -38
  35. data/lib/patient_http/request.rb +70 -46
  36. data/lib/patient_http/request_error.rb +47 -42
  37. data/lib/patient_http/request_helper.rb +142 -119
  38. data/lib/patient_http/request_preparer.rb +13 -10
  39. data/lib/patient_http/request_task.rb +113 -84
  40. data/lib/patient_http/request_template.rb +87 -64
  41. data/lib/patient_http/response.rb +58 -52
  42. data/lib/patient_http/response_reader.rb +66 -65
  43. data/lib/patient_http/secret_manager.rb +34 -30
  44. data/lib/patient_http/secret_reference.rb +33 -26
  45. data/lib/patient_http/synchronous_executor.rb +67 -95
  46. data/lib/patient_http/task_handler.rb +23 -19
  47. data/lib/patient_http/time_helper.rb +8 -8
  48. data/lib/patient_http.rb +311 -186
  49. data/patient_http.gemspec +3 -2
  50. metadata +21 -5
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PatientHttp
4
- # Represents an async HTTP request that will be processed by the async processor.
4
+ # An HTTP request that a processor runs asynchronously.
5
5
  #
6
- # @example Creating a request
6
+ # @example Create a GET request
7
7
  # request = PatientHttp::Request.new(:get, "https://api.example.com/users/123")
8
8
  #
9
- # @example Creating a POST request with JSON body
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
- # Valid HTTP methods
19
+ # The supported HTTP methods.
20
20
  VALID_METHODS = %i[get head post put patch delete query].freeze
21
21
 
22
- # HTTP methods that must not carry a request body
22
+ # The HTTP methods that can't have a request body.
23
23
  BODYLESS_METHODS = %i[get head delete].freeze
24
24
 
25
- # @return [Symbol] HTTP method (:get, :head, :post, :put, :patch, :delete, :query)
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] Request headers
36
+ # @return [HttpHeaders] The request headers.
32
37
  attr_reader :headers
33
38
 
34
- # @return [Numeric, nil] Overall timeout in seconds
39
+ # @return [Numeric, nil] The timeout in seconds for the full request.
35
40
  attr_reader :timeout
36
41
 
37
- # @return [Integer, nil] Maximum number of redirects to follow (nil uses config default, 0 disables)
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 requires changing the HTTP method
41
- # (for example POST to GET on a 302) may be followed (nil uses config default)
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>] Lowercase header names stripped from redirected requests,
45
- # in addition to those configured on the {Configuration}
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}] Query parameters whose values are
49
- # secret references, kept out of the serialized URL and resolved at send time
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>] Names of preprocessors registered on the configuration
53
- # to apply to the request when it is sent
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] Name of the processor that should execute the request.
57
- # Integrations use this to route the request to a named processor; nil
58
- # uses the default processor.
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
- # Reconstruct a Request from a hash
70
+ # Creates a request from its serialized form.
63
71
  #
64
- # @param hash [Hash] hash representation
65
- # @return [Request] reconstructed 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
- # Initializes a new Request.
109
+ # Creates a request.
102
110
  #
103
- # @param http_method [Symbol, String] HTTP method (:get, :head, :post, :put, :patch, :delete, :query).
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] Request headers.
106
- # @param body [String, nil] Request body.
107
- # @param json [Object, nil] JSON body to be serialized (alternative to body).
108
- # @param params [Hash, nil] Query parameters to append to the URL.
109
- # @param timeout [Numeric, nil] Overall timeout in seconds.
110
- # @param max_redirects [Integer, nil] Maximum redirects to follow (nil uses config, 0 disables).
111
- # @param follow_method_changing_redirects [Boolean, nil] Whether to follow a redirect that requires changing
112
- # the HTTP method (nil uses config). When false, such a redirect response is returned as the
113
- # result instead of being followed.
114
- # @param redirect_strip_headers [String, Array<String>, nil] Header names (case insensitive)
115
- # to strip from redirected requests, in addition to those configured on the
116
- # {Configuration}.
117
- # @param preprocessors [String, Symbol, Array<String, Symbol>, nil] Names of preprocessors
118
- # registered on the configuration to apply to the request when it is sent.
119
- # @param processor [String, Symbol, nil] Name of the processor that should execute the
120
- # request. Integrations use this to route the request to a named processor.
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, decoding it from the payload if necessary.
184
+ # Returns the request body. The body is decoded on first access.
169
185
  #
170
- # @return [String, nil] The decoded request body or nil if there was no body.
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
- # Serialize to JSON hash.
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
- # Error object representing an exception from making an HTTP request. Note that this
5
- # is not for HTTP error responses (4xx/5xx), but from actual exceptions raised
6
- # during the request (timeouts, connection errors, SSL errors, etc).
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
- # This is how errors are passed back to the error continuation jobs for processing.
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
- # Valid error types
11
+ # The valid error types.
11
12
  ERROR_TYPES = [:timeout, :connection, :ssl, :response_too_large, :unknown].freeze
12
13
 
13
- # @return [String] Request URL
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] Request duration in seconds
20
+ # @return [Float] The request duration in seconds.
20
21
  attr_reader :duration
21
22
 
22
- # @return [String] Unique request identifier
23
+ # @return [String] The unique request ID.
23
24
  attr_reader :request_id
24
25
 
25
- # @return [Symbol] Categorized error type. This provides a higher level categorization
26
- # of the error (e.g., :connection is used to group IO and socket errors).
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
- # Reconstruct a RequestError from a hash
31
+ # Creates an error from its serialized form.
31
32
  #
32
- # @param hash [Hash] hash representation
33
- # @return [RequestError] reconstructed error
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
- # Create a RequestError from an exception using pattern matching
49
+ # Creates an error from an exception.
49
50
  #
50
- # @param exception [Exception] the exception to convert
51
- # @param duration [Float] request duration in seconds
52
- # @param request_id [String] the request ID
53
- # @param url [String] the request URL
54
- # @param http_method [Symbol, String] the HTTP method
55
- # @param callback_args [Hash, nil] callback arguments (string keys)
56
- # @return [RequestError] the error object
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
- # Determine error type from exception.
74
+ # Returns the error type for an exception.
74
75
  #
75
- # @param exception [Exception] the exception to categorize
76
- # @return [Symbol] the error type
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
- # Initializes a new RequestError.
99
+ # Creates an error.
96
100
  #
97
- # @param class_name [String] Name of the exception class
98
- # @param message [String] Exception message
99
- # @param backtrace [Array<String>] Exception backtrace
100
- # @param error_type [Symbol] Categorized error type
101
- # @param duration [Float] Request duration in seconds
102
- # @param request_id [String] Unique request identifier
103
- # @param url [String] Request URL
104
- # @param http_method [Symbol, String] HTTP method
105
- # @param callback_args [Hash, nil] callback arguments (string keys)
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
- # Convert to hash with string keys for serialization
123
+ # Returns the error as a JSON-compatible hash.
120
124
  #
121
- # @return [Hash] hash representation
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
- # Get the actual Exception class constant from the class_name
140
+ # Returns the class of the exception that caused the error.
137
141
  #
138
- # @return [Class, nil] the exception class or nil if not found
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 as a CallbackArgs object.
148
+ # Returns the callback arguments that were passed with the request.
144
149
  #
145
- # @return [CallbackArgs] the callback arguments
150
+ # @return [CallbackArgs] The callback arguments.
146
151
  def callback_args
147
152
  @callback_args ||= CallbackArgs.load(@callback_args_data)
148
153
  end